mirror of
https://github.com/overte-org/overte.git
synced 2025-04-17 05:08:48 +02:00
Merge remote-tracking branch 'upstream/master' into matFallthrough
This commit is contained in:
commit
8d2e81a13b
297 changed files with 9934 additions and 5123 deletions
.gitignore
android
assignment-client
interface
icon
resources
controllers
fonts
icons
images
qml
+android
Stats.qmlcontrolsUit
hifi
AvatarApp.qmlAvatarPackagerWindow.qml
avatarPackager
AvatarPackagerApp.qmlAvatarPackagerFooter.qmlAvatarPackagerHeader.qmlAvatarPackagerState.qmlAvatarProject.qmlAvatarProjectCard.qmlAvatarProjectUpload.qmlAvatarUploadStatusItem.qmlClickableArea.qmlCreateAvatarProject.qmlInfoBox.qmlLoadingCircle.qmlProjectInputControl.qmlRalewayButton.qmlStyle.qmlqmldir
avatarapp
tablet
src
Application.cppApplication.hAvatarBookmarks.cppMenu.cppMenu.h
avatar
AvatarManager.cppAvatarMotionState.cppAvatarMotionState.hAvatarPackager.cppAvatarPackager.hAvatarProject.cppAvatarProject.hMarketplaceItemUploader.cppMarketplaceItemUploader.hMyAvatar.cppMyAvatar.hOtherAvatar.cppOtherAvatar.h
raypick
scripting
ClipboardScriptingInterface.cppClipboardScriptingInterface.hControllerScriptingInterface.hMenuScriptingInterface.cppPlatformInfoScriptingInterface.cppPlatformInfoScriptingInterface.h
ui
libraries
animation/src
audio-client/src
audio/src
avatars-renderer/src/avatars-renderer
avatars/src
controllers/src/controllers
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -98,6 +98,7 @@ tools/jsdoc/package-lock.json
|
|||
|
||||
# ignore unneeded unity project files for avatar exporter
|
||||
tools/unity-avatar-exporter/Library
|
||||
tools/unity-avatar-exporter/Logs
|
||||
tools/unity-avatar-exporter/Packages
|
||||
tools/unity-avatar-exporter/ProjectSettings
|
||||
tools/unity-avatar-exporter/Temp
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
set -xeuo pipefail
|
||||
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} setupDependencies
|
||||
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} app:${ANDROID_BUILD_TARGET}
|
||||
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} setupDependencies
|
||||
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} app:${ANDROID_BUILD_TARGET}
|
|
@ -9,14 +9,19 @@ docker run \
|
|||
--rm \
|
||||
--security-opt seccomp:unconfined \
|
||||
-v "${WORKSPACE}":/home/jenkins/hifi \
|
||||
-e "RELEASE_NUMBER=${RELEASE_NUMBER}" \
|
||||
-e "RELEASE_TYPE=${RELEASE_TYPE}" \
|
||||
-e "ANDROID_BUILD_TARGET=assembleDebug" \
|
||||
-e "CMAKE_BACKTRACE_URL=${CMAKE_BACKTRACE_URL}" \
|
||||
-e "CMAKE_BACKTRACE_TOKEN=${CMAKE_BACKTRACE_TOKEN}" \
|
||||
-e "CMAKE_BACKTRACE_SYMBOLS_TOKEN=${CMAKE_BACKTRACE_SYMBOLS_TOKEN}" \
|
||||
-e "GA_TRACKING_ID=${GA_TRACKING_ID}" \
|
||||
-e "GIT_PR_COMMIT=${GIT_PR_COMMIT}" \
|
||||
-e "VERSION_CODE=${VERSION_CODE}" \
|
||||
-e RELEASE_NUMBER \
|
||||
-e RELEASE_TYPE \
|
||||
-e ANDROID_BUILD_TARGET \
|
||||
-e ANDROID_BUILD_DIR \
|
||||
-e CMAKE_BACKTRACE_URL \
|
||||
-e CMAKE_BACKTRACE_TOKEN \
|
||||
-e CMAKE_BACKTRACE_SYMBOLS_TOKEN \
|
||||
-e GA_TRACKING_ID \
|
||||
-e OAUTH_CLIENT_SECRET \
|
||||
-e OAUTH_CLIENT_ID \
|
||||
-e OAUTH_REDIRECT_URI \
|
||||
-e VERSION_CODE \
|
||||
"${DOCKER_IMAGE_NAME}" \
|
||||
sh -c "./build_android.sh"
|
||||
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ link_hifi_libraries(
|
|||
audio avatars octree gpu graphics shaders fbx hfm entities
|
||||
networking animation recording shared script-engine embedded-webserver
|
||||
controllers physics plugins midi image
|
||||
model-networking ktx shaders
|
||||
)
|
||||
|
||||
add_dependencies(${TARGET_NAME} oven)
|
||||
|
|
|
@ -915,59 +915,52 @@ void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, Sha
|
|||
void AssetServer::sendStatsPacket() {
|
||||
QJsonObject serverStats;
|
||||
|
||||
auto stats = DependencyManager::get<NodeList>()->sampleStatsForAllConnections();
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
nodeList->eachNode([&](auto& node) {
|
||||
auto& stats = node->getConnectionStats();
|
||||
|
||||
for (const auto& stat : stats) {
|
||||
QJsonObject nodeStats;
|
||||
auto endTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(stat.second.endTime);
|
||||
auto endTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(stats.endTime);
|
||||
QDateTime date = QDateTime::fromMSecsSinceEpoch(endTimeMs.count());
|
||||
|
||||
static const float USEC_PER_SEC = 1000000.0f;
|
||||
static const float MEGABITS_PER_BYTE = 8.0f / 1000000.0f; // Bytes => Mbits
|
||||
float elapsed = (float)(stat.second.endTime - stat.second.startTime).count() / USEC_PER_SEC; // sec
|
||||
float elapsed = (float)(stats.endTime - stats.startTime).count() / USEC_PER_SEC; // sec
|
||||
float megabitsPerSecPerByte = MEGABITS_PER_BYTE / elapsed; // Bytes => Mb/s
|
||||
|
||||
QJsonObject connectionStats;
|
||||
connectionStats["1. Last Heard"] = date.toString();
|
||||
connectionStats["2. Est. Max (P/s)"] = stat.second.estimatedBandwith;
|
||||
connectionStats["3. RTT (ms)"] = stat.second.rtt;
|
||||
connectionStats["4. CW (P)"] = stat.second.congestionWindowSize;
|
||||
connectionStats["5. Period (us)"] = stat.second.packetSendPeriod;
|
||||
connectionStats["6. Up (Mb/s)"] = stat.second.sentBytes * megabitsPerSecPerByte;
|
||||
connectionStats["7. Down (Mb/s)"] = stat.second.receivedBytes * megabitsPerSecPerByte;
|
||||
connectionStats["2. Est. Max (P/s)"] = stats.estimatedBandwith;
|
||||
connectionStats["3. RTT (ms)"] = stats.rtt;
|
||||
connectionStats["4. CW (P)"] = stats.congestionWindowSize;
|
||||
connectionStats["5. Period (us)"] = stats.packetSendPeriod;
|
||||
connectionStats["6. Up (Mb/s)"] = stats.sentBytes * megabitsPerSecPerByte;
|
||||
connectionStats["7. Down (Mb/s)"] = stats.receivedBytes * megabitsPerSecPerByte;
|
||||
nodeStats["Connection Stats"] = connectionStats;
|
||||
|
||||
using Events = udt::ConnectionStats::Stats::Event;
|
||||
const auto& events = stat.second.events;
|
||||
const auto& events = stats.events;
|
||||
|
||||
QJsonObject upstreamStats;
|
||||
upstreamStats["1. Sent (P/s)"] = stat.second.sendRate;
|
||||
upstreamStats["2. Sent Packets"] = stat.second.sentPackets;
|
||||
upstreamStats["1. Sent (P/s)"] = stats.sendRate;
|
||||
upstreamStats["2. Sent Packets"] = (int)stats.sentPackets;
|
||||
upstreamStats["3. Recvd ACK"] = events[Events::ReceivedACK];
|
||||
upstreamStats["4. Procd ACK"] = events[Events::ProcessedACK];
|
||||
upstreamStats["5. Retransmitted"] = events[Events::Retransmission];
|
||||
upstreamStats["5. Retransmitted"] = (int)stats.retransmittedPackets;
|
||||
nodeStats["Upstream Stats"] = upstreamStats;
|
||||
|
||||
QJsonObject downstreamStats;
|
||||
downstreamStats["1. Recvd (P/s)"] = stat.second.receiveRate;
|
||||
downstreamStats["2. Recvd Packets"] = stat.second.receivedPackets;
|
||||
downstreamStats["1. Recvd (P/s)"] = stats.receiveRate;
|
||||
downstreamStats["2. Recvd Packets"] = (int)stats.receivedPackets;
|
||||
downstreamStats["3. Sent ACK"] = events[Events::SentACK];
|
||||
downstreamStats["4. Duplicates"] = events[Events::Duplicate];
|
||||
downstreamStats["4. Duplicates"] = (int)stats.duplicatePackets;
|
||||
nodeStats["Downstream Stats"] = downstreamStats;
|
||||
|
||||
QString uuid;
|
||||
auto nodelist = DependencyManager::get<NodeList>();
|
||||
if (stat.first == nodelist->getDomainHandler().getSockAddr()) {
|
||||
uuid = uuidStringWithoutCurlyBraces(nodelist->getDomainHandler().getUUID());
|
||||
nodeStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = "DomainServer";
|
||||
} else {
|
||||
auto node = nodelist->findNodeWithAddr(stat.first);
|
||||
uuid = uuidStringWithoutCurlyBraces(node ? node->getUUID() : QUuid());
|
||||
nodeStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuid;
|
||||
}
|
||||
QString uuid = uuidStringWithoutCurlyBraces(node->getUUID());
|
||||
nodeStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuid;
|
||||
|
||||
serverStats[uuid] = nodeStats;
|
||||
}
|
||||
});
|
||||
|
||||
// send off the stats packets
|
||||
ThreadedAssignment::addPacketStatsAndSendStatsPacket(serverStats);
|
||||
|
|
|
@ -338,7 +338,7 @@ void AudioMixer::sendStatsPacket() {
|
|||
QJsonObject nodeStats;
|
||||
QString uuidString = uuidStringWithoutCurlyBraces(node->getUUID());
|
||||
|
||||
nodeStats["outbound_kbps"] = node->getOutboundBandwidth();
|
||||
nodeStats["outbound_kbps"] = node->getOutboundKbps();
|
||||
nodeStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuidString;
|
||||
|
||||
nodeStats["jitter"] = clientData->getAudioStreamStats();
|
||||
|
|
|
@ -504,7 +504,7 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre
|
|||
float distance = glm::max(glm::length(relativePosition), EPSILON);
|
||||
float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition);
|
||||
|
||||
float gain = 1.0f;
|
||||
float gain = masterListenerGain;
|
||||
if (!isSoloing) {
|
||||
gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho);
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
|
|||
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
|
||||
packetReceiver.registerListener(PacketType::AvatarIdentityRequest, this, "handleAvatarIdentityRequestPacket");
|
||||
packetReceiver.registerListener(PacketType::SetAvatarTraits, this, "queueIncomingPacket");
|
||||
packetReceiver.registerListener(PacketType::BulkAvatarTraitsAck, this, "queueIncomingPacket");
|
||||
|
||||
packetReceiver.registerListenerForTypes({
|
||||
PacketType::ReplicatedAvatarIdentity,
|
||||
|
@ -746,65 +747,30 @@ void AvatarMixer::sendStatsPacket() {
|
|||
|
||||
|
||||
AvatarMixerSlaveStats aggregateStats;
|
||||
QJsonObject slavesObject;
|
||||
|
||||
float secondsSinceLastStats = (float)(start - _lastStatsTime) / (float)USECS_PER_SECOND;
|
||||
// gather stats
|
||||
int slaveNumber = 1;
|
||||
_slavePool.each([&](AvatarMixerSlave& slave) {
|
||||
QJsonObject slaveObject;
|
||||
AvatarMixerSlaveStats stats;
|
||||
slave.harvestStats(stats);
|
||||
slaveObject["recevied_1_nodesProcessed"] = TIGHT_LOOP_STAT(stats.nodesProcessed);
|
||||
slaveObject["received_2_numPacketsReceived"] = TIGHT_LOOP_STAT(stats.packetsProcessed);
|
||||
|
||||
slaveObject["sent_1_nodesBroadcastedTo"] = TIGHT_LOOP_STAT(stats.nodesBroadcastedTo);
|
||||
slaveObject["sent_2_numBytesSent"] = TIGHT_LOOP_STAT(stats.numBytesSent);
|
||||
slaveObject["sent_3_numPacketsSent"] = TIGHT_LOOP_STAT(stats.numPacketsSent);
|
||||
slaveObject["sent_4_numIdentityPackets"] = TIGHT_LOOP_STAT(stats.numIdentityPackets);
|
||||
|
||||
float averageNodes = ((float)stats.nodesBroadcastedTo / (float)tightLoopFrames);
|
||||
float averageOutboundAvatarKbps = averageNodes ? ((stats.numBytesSent / secondsSinceLastStats) / BYTES_PER_KILOBIT) / averageNodes : 0.0f;
|
||||
slaveObject["sent_5_averageOutboundAvatarKbps"] = averageOutboundAvatarKbps;
|
||||
|
||||
float averageOthersIncluded = averageNodes ? stats.numOthersIncluded / averageNodes : 0.0f;
|
||||
slaveObject["sent_6_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
|
||||
|
||||
float averageOverBudgetAvatars = averageNodes ? stats.overBudgetAvatars / averageNodes : 0.0f;
|
||||
slaveObject["sent_7_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
|
||||
|
||||
slaveObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(stats.processIncomingPacketsElapsedTime);
|
||||
slaveObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(stats.ignoreCalculationElapsedTime);
|
||||
slaveObject["timing_3_toByteArray"] = TIGHT_LOOP_STAT_UINT64(stats.toByteArrayElapsedTime);
|
||||
slaveObject["timing_4_avatarDataPacking"] = TIGHT_LOOP_STAT_UINT64(stats.avatarDataPackingElapsedTime);
|
||||
slaveObject["timing_5_packetSending"] = TIGHT_LOOP_STAT_UINT64(stats.packetSendingElapsedTime);
|
||||
slaveObject["timing_6_jobElapsedTime"] = TIGHT_LOOP_STAT_UINT64(stats.jobElapsedTime);
|
||||
|
||||
slavesObject[QString::number(slaveNumber)] = slaveObject;
|
||||
slaveNumber++;
|
||||
|
||||
aggregateStats += stats;
|
||||
});
|
||||
|
||||
QJsonObject slavesAggregatObject;
|
||||
|
||||
slavesAggregatObject["recevied_1_nodesProcessed"] = TIGHT_LOOP_STAT(aggregateStats.nodesProcessed);
|
||||
slavesAggregatObject["received_2_numPacketsReceived"] = TIGHT_LOOP_STAT(aggregateStats.packetsProcessed);
|
||||
slavesAggregatObject["received_1_nodesProcessed"] = TIGHT_LOOP_STAT(aggregateStats.nodesProcessed);
|
||||
|
||||
slavesAggregatObject["sent_1_nodesBroadcastedTo"] = TIGHT_LOOP_STAT(aggregateStats.nodesBroadcastedTo);
|
||||
slavesAggregatObject["sent_2_numBytesSent"] = TIGHT_LOOP_STAT(aggregateStats.numBytesSent);
|
||||
slavesAggregatObject["sent_3_numPacketsSent"] = TIGHT_LOOP_STAT(aggregateStats.numPacketsSent);
|
||||
slavesAggregatObject["sent_4_numIdentityPackets"] = TIGHT_LOOP_STAT(aggregateStats.numIdentityPackets);
|
||||
|
||||
float averageNodes = ((float)aggregateStats.nodesBroadcastedTo / (float)tightLoopFrames);
|
||||
float averageOutboundAvatarKbps = averageNodes ? ((aggregateStats.numBytesSent / secondsSinceLastStats) / BYTES_PER_KILOBIT) / averageNodes : 0.0f;
|
||||
slavesAggregatObject["sent_5_averageOutboundAvatarKbps"] = averageOutboundAvatarKbps;
|
||||
|
||||
float averageOthersIncluded = averageNodes ? aggregateStats.numOthersIncluded / averageNodes : 0.0f;
|
||||
slavesAggregatObject["sent_6_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
|
||||
slavesAggregatObject["sent_2_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
|
||||
|
||||
float averageOverBudgetAvatars = averageNodes ? aggregateStats.overBudgetAvatars / averageNodes : 0.0f;
|
||||
slavesAggregatObject["sent_7_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
|
||||
slavesAggregatObject["sent_3_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
|
||||
slavesAggregatObject["sent_4_averageDataBytes"] = TIGHT_LOOP_STAT(aggregateStats.numDataBytesSent);
|
||||
slavesAggregatObject["sent_5_averageTraitsBytes"] = TIGHT_LOOP_STAT(aggregateStats.numTraitsBytesSent);
|
||||
slavesAggregatObject["sent_6_averageIdentityBytes"] = TIGHT_LOOP_STAT(aggregateStats.numIdentityBytesSent);
|
||||
|
||||
slavesAggregatObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.processIncomingPacketsElapsedTime);
|
||||
slavesAggregatObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.ignoreCalculationElapsedTime);
|
||||
|
@ -813,8 +779,7 @@ void AvatarMixer::sendStatsPacket() {
|
|||
slavesAggregatObject["timing_5_packetSending"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.packetSendingElapsedTime);
|
||||
slavesAggregatObject["timing_6_jobElapsedTime"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.jobElapsedTime);
|
||||
|
||||
statsObject["slaves_aggregate"] = slavesAggregatObject;
|
||||
statsObject["slaves_individual"] = slavesObject;
|
||||
statsObject["slaves_aggregate (per frame)"] = slavesAggregatObject;
|
||||
|
||||
_handleViewFrustumPacketElapsedTime = 0;
|
||||
_handleAvatarIdentityPacketElapsedTime = 0;
|
||||
|
@ -839,8 +804,9 @@ void AvatarMixer::sendStatsPacket() {
|
|||
// add the key to ask the domain-server for a username replacement, if it has it
|
||||
avatarStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuidStringWithoutCurlyBraces(node->getUUID());
|
||||
|
||||
avatarStats[NODE_OUTBOUND_KBPS_STAT_KEY] = node->getOutboundBandwidth();
|
||||
avatarStats[NODE_INBOUND_KBPS_STAT_KEY] = node->getInboundBandwidth();
|
||||
float outboundAvatarDataKbps = node->getOutboundKbps();
|
||||
avatarStats[NODE_OUTBOUND_KBPS_STAT_KEY] = outboundAvatarDataKbps;
|
||||
avatarStats[NODE_INBOUND_KBPS_STAT_KEY] = node->getInboundKbps();
|
||||
|
||||
AvatarMixerClientData* clientData = static_cast<AvatarMixerClientData*>(node->getLinkedData());
|
||||
if (clientData) {
|
||||
|
@ -850,7 +816,7 @@ void AvatarMixer::sendStatsPacket() {
|
|||
|
||||
// add the diff between the full outbound bandwidth and the measured bandwidth for AvatarData send only
|
||||
avatarStats["delta_full_vs_avatar_data_kbps"] =
|
||||
avatarStats[NODE_OUTBOUND_KBPS_STAT_KEY].toDouble() - avatarStats[OUTBOUND_AVATAR_DATA_STATS_KEY].toDouble();
|
||||
(double)outboundAvatarDataKbps - avatarStats[OUTBOUND_AVATAR_DATA_STATS_KEY].toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,9 +19,8 @@
|
|||
|
||||
#include "AvatarMixerSlave.h"
|
||||
|
||||
AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID) :
|
||||
NodeData(nodeID, nodeLocalID)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
@ -68,6 +67,9 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData
|
|||
case PacketType::SetAvatarTraits:
|
||||
processSetTraitsMessage(*packet, slaveSharedData, *node);
|
||||
break;
|
||||
case PacketType::BulkAvatarTraitsAck:
|
||||
processBulkAvatarTraitsAckMessage(*packet);
|
||||
break;
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
|
@ -79,12 +81,11 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData
|
|||
}
|
||||
|
||||
int AvatarMixerClientData::parseData(ReceivedMessage& message) {
|
||||
|
||||
// pull the sequence number from the data first
|
||||
uint16_t sequenceNumber;
|
||||
|
||||
message.readPrimitive(&sequenceNumber);
|
||||
|
||||
|
||||
if (sequenceNumber < _lastReceivedSequenceNumber && _lastReceivedSequenceNumber != UINT16_MAX) {
|
||||
incrementNumOutOfOrderSends();
|
||||
}
|
||||
|
@ -95,7 +96,8 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message) {
|
|||
}
|
||||
|
||||
void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
|
||||
const SlaveSharedData& slaveSharedData, Node& sendingNode) {
|
||||
const SlaveSharedData& slaveSharedData,
|
||||
Node& sendingNode) {
|
||||
// pull the trait version from the message
|
||||
AvatarTraits::TraitVersion packetTraitVersion;
|
||||
message.readPrimitive(&packetTraitVersion);
|
||||
|
@ -134,7 +136,7 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
|
|||
AvatarTraits::TraitInstanceID instanceID = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID));
|
||||
|
||||
if (message.getBytesLeftToRead() == 0) {
|
||||
qWarning () << "Received an instanced trait with no size from" << message.getSenderSockAddr();
|
||||
qWarning() << "Received an instanced trait with no size from" << message.getSenderSockAddr();
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -142,7 +144,8 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
|
|||
message.readPrimitive(&traitSize);
|
||||
|
||||
if (traitSize < -1 || traitSize > message.getBytesLeftToRead()) {
|
||||
qWarning() << "Refusing to process instanced trait of size" << traitSize << "from" << message.getSenderSockAddr();
|
||||
qWarning() << "Refusing to process instanced trait of size" << traitSize << "from"
|
||||
<< message.getSenderSockAddr();
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -154,7 +157,7 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
|
|||
if (traitSize == AvatarTraits::DELETED_TRAIT_SIZE) {
|
||||
_avatar->processDeletedTraitInstance(traitType, instanceID);
|
||||
// Mixer doesn't need deleted IDs.
|
||||
_avatar->getAndClearRecentlyDetachedIDs();
|
||||
_avatar->getAndClearRecentlyRemovedIDs();
|
||||
|
||||
// to track a deleted instance but keep version information
|
||||
// the avatar mixer uses the negative value of the sent version
|
||||
|
@ -169,7 +172,8 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
|
|||
message.seek(message.getPosition() + traitSize);
|
||||
}
|
||||
} else {
|
||||
qWarning() << "Refusing to process traits packet with instanced trait of unprocessable type from" << message.getSenderSockAddr();
|
||||
qWarning() << "Refusing to process traits packet with instanced trait of unprocessable type from"
|
||||
<< message.getSenderSockAddr();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -180,7 +184,63 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
|
|||
}
|
||||
}
|
||||
|
||||
void AvatarMixerClientData::checkSkeletonURLAgainstWhitelist(const SlaveSharedData &slaveSharedData, Node& sendingNode,
|
||||
void AvatarMixerClientData::processBulkAvatarTraitsAckMessage(ReceivedMessage& message) {
|
||||
// Avatar Traits flow control marks each outgoing avatar traits packet with a
|
||||
// sequence number. The mixer caches the traits sent in the traits packet.
|
||||
// Until an ack with the sequence number comes back, all updates to _traits
|
||||
// in that packet_ are ignored. Updates to traits not in that packet will
|
||||
// be sent.
|
||||
|
||||
// Look up the avatar/trait data associated with this ack and update the 'last ack' list
|
||||
// with it.
|
||||
AvatarTraits::TraitMessageSequence seq;
|
||||
message.readPrimitive(&seq);
|
||||
auto sentAvatarTraitVersions = _perNodePendingTraitVersions.find(seq);
|
||||
if (sentAvatarTraitVersions != _perNodePendingTraitVersions.end()) {
|
||||
for (auto& perNodeTraitVersions : sentAvatarTraitVersions->second) {
|
||||
auto& nodeId = perNodeTraitVersions.first;
|
||||
auto& traitVersions = perNodeTraitVersions.second;
|
||||
// For each trait that was sent in the traits packet,
|
||||
// update the 'acked' trait version. Traits not
|
||||
// sent in the traits packet keep their version.
|
||||
|
||||
// process simple traits
|
||||
auto simpleReceivedIt = traitVersions.simpleCBegin();
|
||||
while (simpleReceivedIt != traitVersions.simpleCEnd()) {
|
||||
if (*simpleReceivedIt != AvatarTraits::DEFAULT_TRAIT_VERSION) {
|
||||
auto traitType = static_cast<AvatarTraits::TraitType>(std::distance(traitVersions.simpleCBegin(), simpleReceivedIt));
|
||||
_perNodeAckedTraitVersions[nodeId][traitType] = *simpleReceivedIt;
|
||||
}
|
||||
simpleReceivedIt++;
|
||||
}
|
||||
|
||||
// process instanced traits
|
||||
auto instancedSentIt = traitVersions.instancedCBegin();
|
||||
while (instancedSentIt != traitVersions.instancedCEnd()) {
|
||||
auto traitType = instancedSentIt->traitType;
|
||||
|
||||
for (auto& sentInstance : instancedSentIt->instances) {
|
||||
auto instanceID = sentInstance.id;
|
||||
const auto sentVersion = sentInstance.value;
|
||||
_perNodeAckedTraitVersions[nodeId].instanceInsert(traitType, instanceID, sentVersion);
|
||||
}
|
||||
instancedSentIt++;
|
||||
}
|
||||
}
|
||||
_perNodePendingTraitVersions.erase(sentAvatarTraitVersions);
|
||||
} else {
|
||||
// This can happen either the BulkAvatarTraits was sent with no simple traits,
|
||||
// or if the avatar mixer restarts while there are pending
|
||||
// BulkAvatarTraits messages in-flight.
|
||||
if (seq > getTraitsMessageSequence()) {
|
||||
qWarning() << "Received BulkAvatarTraitsAck with future seq (potential avatar mixer restart) " << seq << " from "
|
||||
<< message.getSenderSockAddr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AvatarMixerClientData::checkSkeletonURLAgainstWhitelist(const SlaveSharedData& slaveSharedData,
|
||||
Node& sendingNode,
|
||||
AvatarTraits::TraitVersion traitVersion) {
|
||||
const auto& whitelist = slaveSharedData.skeletonURLWhitelist;
|
||||
|
||||
|
@ -282,14 +342,18 @@ void AvatarMixerClientData::removeFromRadiusIgnoringSet(const QUuid& other) {
|
|||
|
||||
void AvatarMixerClientData::resetSentTraitData(Node::LocalID nodeLocalID) {
|
||||
_lastSentTraitsTimestamps[nodeLocalID] = TraitsCheckTimestamp();
|
||||
_sentTraitVersions[nodeLocalID].reset();
|
||||
_perNodeSentTraitVersions[nodeLocalID].reset();
|
||||
_perNodeAckedTraitVersions[nodeLocalID].reset();
|
||||
for (auto && pendingTraitVersions : _perNodePendingTraitVersions) {
|
||||
pendingTraitVersions.second[nodeLocalID].reset();
|
||||
}
|
||||
}
|
||||
|
||||
void AvatarMixerClientData::readViewFrustumPacket(const QByteArray& message) {
|
||||
_currentViewFrustums.clear();
|
||||
|
||||
auto sourceBuffer = reinterpret_cast<const unsigned char*>(message.constData());
|
||||
|
||||
|
||||
uint8_t numFrustums = 0;
|
||||
memcpy(&numFrustums, sourceBuffer, sizeof(numFrustums));
|
||||
sourceBuffer += sizeof(numFrustums);
|
||||
|
@ -317,7 +381,8 @@ void AvatarMixerClientData::loadJSONStats(QJsonObject& jsonObject) const {
|
|||
jsonObject["total_num_out_of_order_sends"] = _numOutOfOrderSends;
|
||||
|
||||
jsonObject[OUTBOUND_AVATAR_DATA_STATS_KEY] = getOutboundAvatarDataKbps();
|
||||
jsonObject[INBOUND_AVATAR_DATA_STATS_KEY] = _avatar->getAverageBytesReceivedPerSecond() / (float) BYTES_PER_KILOBIT;
|
||||
jsonObject[OUTBOUND_AVATAR_TRAITS_STATS_KEY] = getOutboundAvatarTraitsKbps();
|
||||
jsonObject[INBOUND_AVATAR_DATA_STATS_KEY] = _avatar->getAverageBytesReceivedPerSecond() / (float)BYTES_PER_KILOBIT;
|
||||
|
||||
jsonObject["av_data_receive_rate"] = _avatar->getReceiveRate();
|
||||
jsonObject["recent_other_av_in_view"] = _recentOtherAvatarsInView;
|
||||
|
@ -338,5 +403,5 @@ void AvatarMixerClientData::cleanupKilledNode(const QUuid&, Node::LocalID nodeLo
|
|||
removeLastBroadcastSequenceNumber(nodeLocalID);
|
||||
removeLastBroadcastTime(nodeLocalID);
|
||||
_lastSentTraitsTimestamps.erase(nodeLocalID);
|
||||
_sentTraitVersions.erase(nodeLocalID);
|
||||
_perNodeSentTraitVersions.erase(nodeLocalID);
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
#include <shared/ConicalViewFrustum.h>
|
||||
|
||||
const QString OUTBOUND_AVATAR_DATA_STATS_KEY = "outbound_av_data_kbps";
|
||||
const QString OUTBOUND_AVATAR_TRAITS_STATS_KEY = "outbound_av_traits_kbps";
|
||||
const QString INBOUND_AVATAR_DATA_STATS_KEY = "inbound_av_data_kbps";
|
||||
|
||||
struct SlaveSharedData;
|
||||
|
@ -42,6 +43,7 @@ public:
|
|||
AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID);
|
||||
virtual ~AvatarMixerClientData() {}
|
||||
using HRCTime = p_high_resolution_clock::time_point;
|
||||
using PerNodeTraitVersions = std::unordered_map<Node::LocalID, AvatarTraits::TraitVersions>;
|
||||
|
||||
int parseData(ReceivedMessage& message) override;
|
||||
AvatarData& getAvatar() { return *_avatar; }
|
||||
|
@ -85,10 +87,15 @@ public:
|
|||
void incrementNumFramesSinceFRDAdjustment() { ++_numFramesSinceAdjustment; }
|
||||
void resetNumFramesSinceFRDAdjustment() { _numFramesSinceAdjustment = 0; }
|
||||
|
||||
void recordSentAvatarData(int numBytes) { _avgOtherAvatarDataRate.updateAverage((float) numBytes); }
|
||||
void recordSentAvatarData(int numDataBytes, int numTraitsBytes = 0) {
|
||||
_avgOtherAvatarDataRate.updateAverage(numDataBytes);
|
||||
_avgOtherAvatarTraitsRate.updateAverage(numTraitsBytes);
|
||||
}
|
||||
|
||||
float getOutboundAvatarDataKbps() const
|
||||
{ return _avgOtherAvatarDataRate.getAverageSampleValuePerSecond() / (float) BYTES_PER_KILOBIT; }
|
||||
float getOutboundAvatarTraitsKbps() const
|
||||
{ return _avgOtherAvatarTraitsRate.getAverageSampleValuePerSecond() / BYTES_PER_KILOBIT; }
|
||||
|
||||
void loadJSONStats(QJsonObject& jsonObject) const;
|
||||
|
||||
|
@ -124,6 +131,7 @@ public:
|
|||
int processPackets(const SlaveSharedData& slaveSharedData); // returns number of packets processed
|
||||
|
||||
void processSetTraitsMessage(ReceivedMessage& message, const SlaveSharedData& slaveSharedData, Node& sendingNode);
|
||||
void processBulkAvatarTraitsAckMessage(ReceivedMessage& message);
|
||||
void checkSkeletonURLAgainstWhitelist(const SlaveSharedData& slaveSharedData, Node& sendingNode,
|
||||
AvatarTraits::TraitVersion traitVersion);
|
||||
|
||||
|
@ -138,7 +146,14 @@ public:
|
|||
void setLastOtherAvatarTraitsSendPoint(Node::LocalID otherAvatar, TraitsCheckTimestamp sendPoint)
|
||||
{ _lastSentTraitsTimestamps[otherAvatar] = sendPoint; }
|
||||
|
||||
AvatarTraits::TraitVersions& getLastSentTraitVersions(Node::LocalID otherAvatar) { return _sentTraitVersions[otherAvatar]; }
|
||||
AvatarTraits::TraitMessageSequence getTraitsMessageSequence() const { return _currentTraitsMessageSequence; }
|
||||
AvatarTraits::TraitMessageSequence nextTraitsMessageSequence() { return ++_currentTraitsMessageSequence; }
|
||||
AvatarTraits::TraitVersions& getPendingTraitVersions(AvatarTraits::TraitMessageSequence seq, Node::LocalID otherId) {
|
||||
return _perNodePendingTraitVersions[seq][otherId];
|
||||
}
|
||||
|
||||
AvatarTraits::TraitVersions& getLastSentTraitVersions(Node::LocalID otherAvatar) { return _perNodeSentTraitVersions[otherAvatar]; }
|
||||
AvatarTraits::TraitVersions& getLastAckedTraitVersions(Node::LocalID otherAvatar) { return _perNodeAckedTraitVersions[otherAvatar]; }
|
||||
|
||||
void resetSentTraitData(Node::LocalID nodeID);
|
||||
|
||||
|
@ -171,6 +186,7 @@ private:
|
|||
int _numOutOfOrderSends = 0;
|
||||
|
||||
SimpleMovingAverage _avgOtherAvatarDataRate;
|
||||
SimpleMovingAverage _avgOtherAvatarTraitsRate;
|
||||
std::vector<QUuid> _radiusIgnoredOthers;
|
||||
ConicalViewFrustums _currentViewFrustums;
|
||||
|
||||
|
@ -183,8 +199,27 @@ private:
|
|||
AvatarTraits::TraitVersions _lastReceivedTraitVersions;
|
||||
TraitsCheckTimestamp _lastReceivedTraitsChange;
|
||||
|
||||
AvatarTraits::TraitMessageSequence _currentTraitsMessageSequence{ 0 };
|
||||
|
||||
// Cache of trait versions sent in a given packet (indexed by sequence number)
|
||||
// When an ack is received, the sequence number in the ack is used to look up
|
||||
// the sent trait versions and they are copied to _perNodeAckedTraitVersions.
|
||||
// We remember the data in _perNodePendingTraitVersions instead of requiring
|
||||
// the client to return all of the versions for each trait it received in a given packet,
|
||||
// reducing the size of the ack packet.
|
||||
std::unordered_map<AvatarTraits::TraitMessageSequence, PerNodeTraitVersions> _perNodePendingTraitVersions;
|
||||
|
||||
// Versions of traits that have been acked, which will be compared to incoming
|
||||
// trait updates. Incoming updates going to a given node will be ignored if
|
||||
// the ack for the previous packet (containing those versions) has not been
|
||||
// received.
|
||||
PerNodeTraitVersions _perNodeAckedTraitVersions;
|
||||
|
||||
std::unordered_map<Node::LocalID, TraitsCheckTimestamp> _lastSentTraitsTimestamps;
|
||||
std::unordered_map<Node::LocalID, AvatarTraits::TraitVersions> _sentTraitVersions;
|
||||
|
||||
// cache of traits sent to a node which are compared to incoming traits to
|
||||
// prevent sending traits that have already been sent.
|
||||
PerNodeTraitVersions _perNodeSentTraitVersions;
|
||||
|
||||
std::atomic_bool _isIgnoreRadiusEnabled { false };
|
||||
};
|
||||
|
|
|
@ -73,52 +73,82 @@ int AvatarMixerSlave::sendIdentityPacket(NLPacketList& packetList, const AvatarM
|
|||
QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray();
|
||||
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious
|
||||
packetList.write(individualData);
|
||||
_stats.numIdentityPackets++;
|
||||
_stats.numIdentityPacketsSent++;
|
||||
_stats.numIdentityBytesSent += individualData.size();
|
||||
return individualData.size();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
qint64 AvatarMixerSlave::addTraitsNodeHeader(AvatarMixerClientData* listeningNodeData,
|
||||
const AvatarMixerClientData* sendingNodeData,
|
||||
NLPacketList& traitsPacketList,
|
||||
qint64 bytesWritten) {
|
||||
if (bytesWritten == 0) {
|
||||
if (traitsPacketList.getNumPackets() == 0) {
|
||||
// This is the beginning of the traits packet, write out the sequence number.
|
||||
bytesWritten += traitsPacketList.writePrimitive(listeningNodeData->nextTraitsMessageSequence());
|
||||
}
|
||||
// This is the beginning of the traits for a node, write out the node id
|
||||
bytesWritten += traitsPacketList.write(sendingNodeData->getNodeID().toRfc4122());
|
||||
}
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* listeningNodeData,
|
||||
const AvatarMixerClientData* sendingNodeData,
|
||||
NLPacketList& traitsPacketList) {
|
||||
|
||||
auto otherNodeLocalID = sendingNodeData->getNodeLocalID();
|
||||
// Avatar Traits flow control marks each outgoing avatar traits packet with a
|
||||
// sequence number. The mixer caches the traits sent in the traits packet.
|
||||
// Until an ack with the sequence number comes back, all updates to _traits
|
||||
// in that packet_ are ignored. Updates to traits not in that packet will
|
||||
// be sent.
|
||||
|
||||
auto sendingNodeLocalID = 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 timeOfLastTraitsSent = listeningNodeData->getLastOtherAvatarTraitsSendPoint(sendingNodeLocalID);
|
||||
auto timeOfLastTraitsChange = sendingNodeData->getLastReceivedTraitsChange();
|
||||
bool allTraitsUpdated = true;
|
||||
|
||||
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);
|
||||
auto& lastSentVersions = listeningNodeData->getLastSentTraitVersions(sendingNodeLocalID);
|
||||
auto& lastAckedVersions = listeningNodeData->getLastAckedTraitVersions(sendingNodeLocalID);
|
||||
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];
|
||||
auto& lastAckedVersionRef = lastAckedVersions[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;
|
||||
// hold sending more traits until we've been acked that the last one we sent was received
|
||||
if (lastSentVersionRef == lastAckedVersionRef) {
|
||||
if (lastReceivedVersion > lastSentVersionRef) {
|
||||
bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten);
|
||||
// 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;
|
||||
// Remember which versions we sent in this particular packet
|
||||
// so we can verify when it's acked.
|
||||
auto& pendingTraitVersions = listeningNodeData->getPendingTraitVersions(listeningNodeData->getTraitsMessageSequence(), sendingNodeLocalID);
|
||||
pendingTraitVersions[traitType] = lastReceivedVersion;
|
||||
}
|
||||
} else {
|
||||
allTraitsUpdated = false;
|
||||
}
|
||||
|
||||
++simpleReceivedIt;
|
||||
|
@ -131,6 +161,7 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis
|
|||
|
||||
// get or create the sent trait versions for this trait type
|
||||
auto& sentIDValuePairs = lastSentVersions.getInstanceIDValuePairs(traitType);
|
||||
auto& ackIDValuePairs = lastAckedVersions.getInstanceIDValuePairs(traitType);
|
||||
|
||||
// enumerate each received instance
|
||||
for (auto& receivedInstance : instancedReceivedIt->instances) {
|
||||
|
@ -148,8 +179,19 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis
|
|||
{
|
||||
return sentInstance.id == instanceID;
|
||||
});
|
||||
|
||||
// look for existing acked version for this instance
|
||||
auto ackedInstanceIt = std::find_if(ackIDValuePairs.begin(), ackIDValuePairs.end(),
|
||||
[instanceID](auto& ackInstance) { return ackInstance.id == instanceID; });
|
||||
|
||||
// if we have a sent version, then we must have an acked instance of the same trait with the same
|
||||
// version to go on, otherwise we drop the received trait
|
||||
if (sentInstanceIt != sentIDValuePairs.end() &&
|
||||
(ackedInstanceIt == ackIDValuePairs.end() || sentInstanceIt->value != ackedInstanceIt->value)) {
|
||||
allTraitsUpdated = false;
|
||||
continue;
|
||||
}
|
||||
if (!isDeleted && (sentInstanceIt == sentIDValuePairs.end() || receivedVersion > sentInstanceIt->value)) {
|
||||
bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten);
|
||||
|
||||
// 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);
|
||||
|
@ -159,25 +201,40 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis
|
|||
} else {
|
||||
sentIDValuePairs.emplace_back(instanceID, receivedVersion);
|
||||
}
|
||||
|
||||
auto& pendingTraitVersions =
|
||||
listeningNodeData->getPendingTraitVersions(listeningNodeData->getTraitsMessageSequence(),
|
||||
sendingNodeLocalID);
|
||||
pendingTraitVersions.instanceInsert(traitType, instanceID, receivedVersion);
|
||||
|
||||
} else if (isDeleted && sentInstanceIt != sentIDValuePairs.end() && absoluteReceivedVersion > sentInstanceIt->value) {
|
||||
bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten);
|
||||
|
||||
// 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;
|
||||
|
||||
auto& pendingTraitVersions =
|
||||
listeningNodeData->getPendingTraitVersions(listeningNodeData->getTraitsMessageSequence(),
|
||||
sendingNodeLocalID);
|
||||
pendingTraitVersions.instanceInsert(traitType, instanceID, 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);
|
||||
if (bytesWritten) {
|
||||
// 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
|
||||
if (allTraitsUpdated) {
|
||||
listeningNodeData->setLastOtherAvatarTraitsSendPoint(sendingNodeLocalID, timeOfLastTraitsChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -191,7 +248,8 @@ int AvatarMixerSlave::sendReplicatedIdentityPacket(const Node& agentNode, const
|
|||
auto identityPacket = NLPacketList::create(PacketType::ReplicatedAvatarIdentity, QByteArray(), true, true);
|
||||
identityPacket->write(individualData);
|
||||
DependencyManager::get<NodeList>()->sendPacketList(std::move(identityPacket), destinationNode);
|
||||
_stats.numIdentityPackets++;
|
||||
_stats.numIdentityPacketsSent++;
|
||||
_stats.numIdentityBytesSent += individualData.size();
|
||||
return individualData.size();
|
||||
} else {
|
||||
return 0;
|
||||
|
@ -419,6 +477,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
|
|||
|
||||
int remainingAvatars = (int)sortedAvatars.size();
|
||||
auto traitsPacketList = NLPacketList::create(PacketType::BulkAvatarTraits, QByteArray(), true, true);
|
||||
|
||||
auto avatarPacket = NLPacket::create(PacketType::BulkAvatarData);
|
||||
const int avatarPacketCapacity = avatarPacket->getPayloadCapacity();
|
||||
int avatarSpaceAvailable = avatarPacketCapacity;
|
||||
|
@ -539,17 +598,16 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
|
|||
++numPacketsSent;
|
||||
}
|
||||
|
||||
_stats.numPacketsSent += numPacketsSent;
|
||||
_stats.numBytesSent += numAvatarDataBytes;
|
||||
|
||||
// record the bytes sent for other avatar data in the AvatarMixerClientData
|
||||
nodeData->recordSentAvatarData(numAvatarDataBytes);
|
||||
_stats.numDataPacketsSent += numPacketsSent;
|
||||
_stats.numDataBytesSent += numAvatarDataBytes;
|
||||
|
||||
// close the current traits packet list
|
||||
traitsPacketList->closeCurrentPacket();
|
||||
|
||||
if (traitsPacketList->getNumPackets() >= 1) {
|
||||
// send the traits packet list
|
||||
_stats.numTraitsBytesSent += traitBytesSent;
|
||||
_stats.numTraitsPacketsSent += (int) traitsPacketList->getNumPackets();
|
||||
nodeList->sendPacketList(std::move(traitsPacketList), *destinationNode);
|
||||
}
|
||||
|
||||
|
@ -559,6 +617,10 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
|
|||
nodeList->sendPacketList(std::move(identityPacketList), *destinationNode);
|
||||
}
|
||||
|
||||
// record the bytes sent for other avatar data in the AvatarMixerClientData
|
||||
nodeData->recordSentAvatarData(numAvatarDataBytes, traitBytesSent);
|
||||
|
||||
|
||||
// record the number of avatars held back this frame
|
||||
nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack);
|
||||
nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames);
|
||||
|
@ -685,8 +747,8 @@ void AvatarMixerSlave::broadcastAvatarDataToDownstreamMixer(const SharedNodePoin
|
|||
// close the current packet so that we're always sending something
|
||||
avatarPacketList->closeCurrentPacket(true);
|
||||
|
||||
_stats.numPacketsSent += (int)avatarPacketList->getNumPackets();
|
||||
_stats.numBytesSent += numAvatarDataBytes;
|
||||
_stats.numDataPacketsSent += (int)avatarPacketList->getNumPackets();
|
||||
_stats.numDataBytesSent += numAvatarDataBytes;
|
||||
|
||||
// send the replicated bulk avatar data
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
|
|
@ -24,9 +24,12 @@ public:
|
|||
|
||||
int nodesBroadcastedTo { 0 };
|
||||
int downstreamMixersBroadcastedTo { 0 };
|
||||
int numPacketsSent { 0 };
|
||||
int numBytesSent { 0 };
|
||||
int numIdentityPackets { 0 };
|
||||
int numDataBytesSent { 0 };
|
||||
int numTraitsBytesSent { 0 };
|
||||
int numIdentityBytesSent { 0 };
|
||||
int numDataPacketsSent { 0 };
|
||||
int numTraitsPacketsSent { 0 };
|
||||
int numIdentityPacketsSent { 0 };
|
||||
int numOthersIncluded { 0 };
|
||||
int overBudgetAvatars { 0 };
|
||||
|
||||
|
@ -45,9 +48,13 @@ public:
|
|||
// sending job stats
|
||||
nodesBroadcastedTo = 0;
|
||||
downstreamMixersBroadcastedTo = 0;
|
||||
numPacketsSent = 0;
|
||||
numBytesSent = 0;
|
||||
numIdentityPackets = 0;
|
||||
|
||||
numDataBytesSent = 0;
|
||||
numTraitsBytesSent = 0;
|
||||
numIdentityBytesSent = 0;
|
||||
numDataPacketsSent = 0;
|
||||
numTraitsPacketsSent = 0;
|
||||
numIdentityPacketsSent = 0;
|
||||
numOthersIncluded = 0;
|
||||
overBudgetAvatars = 0;
|
||||
|
||||
|
@ -65,9 +72,12 @@ public:
|
|||
|
||||
nodesBroadcastedTo += rhs.nodesBroadcastedTo;
|
||||
downstreamMixersBroadcastedTo += rhs.downstreamMixersBroadcastedTo;
|
||||
numPacketsSent += rhs.numPacketsSent;
|
||||
numBytesSent += rhs.numBytesSent;
|
||||
numIdentityPackets += rhs.numIdentityPackets;
|
||||
numDataBytesSent += rhs.numDataBytesSent;
|
||||
numTraitsBytesSent += rhs.numTraitsBytesSent;
|
||||
numIdentityBytesSent += rhs.numIdentityBytesSent;
|
||||
numDataPacketsSent += rhs.numDataPacketsSent;
|
||||
numTraitsPacketsSent += rhs.numTraitsPacketsSent;
|
||||
numIdentityPacketsSent += rhs.numIdentityPacketsSent;
|
||||
numOthersIncluded += rhs.numOthersIncluded;
|
||||
overBudgetAvatars += rhs.overBudgetAvatars;
|
||||
|
||||
|
@ -104,6 +114,11 @@ private:
|
|||
int sendIdentityPacket(NLPacketList& packet, const AvatarMixerClientData* nodeData, const Node& destinationNode);
|
||||
int sendReplicatedIdentityPacket(const Node& agentNode, const AvatarMixerClientData* nodeData, const Node& destinationNode);
|
||||
|
||||
qint64 addTraitsNodeHeader(AvatarMixerClientData* listeningNodeData,
|
||||
const AvatarMixerClientData* sendingNodeData,
|
||||
NLPacketList& traitsPacketList,
|
||||
qint64 bytesWritten);
|
||||
|
||||
qint64 addChangedTraitsToBulkPacket(AvatarMixerClientData* listeningNodeData,
|
||||
const AvatarMixerClientData* sendingNodeData,
|
||||
NLPacketList& traitsPacketList);
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
#include <GLMHelpers.h>
|
||||
#include <ResourceRequestObserver.h>
|
||||
#include <AvatarLogging.h>
|
||||
#include <EntityItem.h>
|
||||
#include <EntityItemProperties.h>
|
||||
|
||||
|
||||
ScriptableAvatar::ScriptableAvatar() {
|
||||
|
@ -249,3 +251,157 @@ void ScriptableAvatar::setHasProceduralEyeFaceMovement(bool hasProceduralEyeFace
|
|||
void ScriptableAvatar::setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement) {
|
||||
_headData->setHasAudioEnabledFaceMovement(hasAudioEnabledFaceMovement);
|
||||
}
|
||||
|
||||
AvatarEntityMap ScriptableAvatar::getAvatarEntityData() const {
|
||||
// DANGER: Now that we store the AvatarEntityData in packed format this call is potentially Very Expensive!
|
||||
// Avoid calling this method if possible.
|
||||
AvatarEntityMap data;
|
||||
QUuid sessionID = getID();
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
for (const auto& itr : _entities) {
|
||||
QUuid id = itr.first;
|
||||
EntityItemPointer entity = itr.second;
|
||||
EntityItemProperties properties = entity->getProperties();
|
||||
QByteArray blob;
|
||||
EntityItemProperties::propertiesToBlob(_scriptEngine, sessionID, properties, blob);
|
||||
data[id] = blob;
|
||||
}
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
void ScriptableAvatar::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
|
||||
// Note: this is an invokable Script call
|
||||
// avatarEntityData is expected to be a map of QByteArrays that represent EntityItemProperties objects from JavaScript
|
||||
//
|
||||
if (avatarEntityData.size() > MAX_NUM_AVATAR_ENTITIES) {
|
||||
// the data is suspect
|
||||
qCDebug(avatars) << "discard suspect avatarEntityData with size =" << avatarEntityData.size();
|
||||
return;
|
||||
}
|
||||
|
||||
// convert binary data to EntityItemProperties
|
||||
// NOTE: this operation is NOT efficient
|
||||
std::map<QUuid, EntityItemProperties> newProperties;
|
||||
AvatarEntityMap::const_iterator dataItr = avatarEntityData.begin();
|
||||
while (dataItr != avatarEntityData.end()) {
|
||||
EntityItemProperties properties;
|
||||
const QByteArray& blob = dataItr.value();
|
||||
if (!blob.isNull() && EntityItemProperties::blobToProperties(_scriptEngine, blob, properties)) {
|
||||
newProperties[dataItr.key()] = properties;
|
||||
}
|
||||
++dataItr;
|
||||
}
|
||||
|
||||
// delete existing entities not found in avatarEntityData
|
||||
std::vector<QUuid> idsToClear;
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
std::map<QUuid, EntityItemPointer>::iterator entityItr = _entities.begin();
|
||||
while (entityItr != _entities.end()) {
|
||||
QUuid id = entityItr->first;
|
||||
std::map<QUuid, EntityItemProperties>::const_iterator propertiesItr = newProperties.find(id);
|
||||
if (propertiesItr == newProperties.end()) {
|
||||
idsToClear.push_back(id);
|
||||
entityItr = _entities.erase(entityItr);
|
||||
} else {
|
||||
++entityItr;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// add or update entities
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
std::map<QUuid, EntityItemProperties>::const_iterator propertiesItr = newProperties.begin();
|
||||
while (propertiesItr != newProperties.end()) {
|
||||
QUuid id = propertiesItr->first;
|
||||
const EntityItemProperties& properties = propertiesItr->second;
|
||||
std::map<QUuid, EntityItemPointer>::iterator entityItr = _entities.find(id);
|
||||
EntityItemPointer entity;
|
||||
if (entityItr != _entities.end()) {
|
||||
entity = entityItr->second;
|
||||
entity->setProperties(properties);
|
||||
} else {
|
||||
entity = EntityTypes::constructEntityItem(id, properties);
|
||||
}
|
||||
if (entity) {
|
||||
// build outgoing payload
|
||||
OctreePacketData packetData(false, AvatarTraits::MAXIMUM_TRAIT_SIZE);
|
||||
EncodeBitstreamParams params;
|
||||
EntityTreeElementExtraEncodeDataPointer extra { nullptr };
|
||||
OctreeElement::AppendState appendState = entity->appendEntityData(&packetData, params, extra);
|
||||
|
||||
if (appendState == OctreeElement::COMPLETED) {
|
||||
_entities[id] = entity;
|
||||
QByteArray tempArray((const char*)packetData.getUncompressedData(), packetData.getUncompressedSize());
|
||||
storeAvatarEntityDataPayload(id, tempArray);
|
||||
} else {
|
||||
// payload doesn't fit
|
||||
entityItr = _entities.find(id);
|
||||
if (entityItr != _entities.end()) {
|
||||
_entities.erase(entityItr);
|
||||
idsToClear.push_back(id);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
++propertiesItr;
|
||||
}
|
||||
});
|
||||
|
||||
// clear deleted traits
|
||||
for (const auto& id : idsToClear) {
|
||||
clearAvatarEntity(id);
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptableAvatar::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) {
|
||||
if (entityData.isNull()) {
|
||||
// interpret this as a DELETE
|
||||
std::map<QUuid, EntityItemPointer>::iterator itr = _entities.find(entityID);
|
||||
if (itr != _entities.end()) {
|
||||
_entities.erase(itr);
|
||||
clearAvatarEntity(entityID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
EntityItemPointer entity;
|
||||
EntityItemProperties properties;
|
||||
if (!EntityItemProperties::blobToProperties(_scriptEngine, entityData, properties)) {
|
||||
// entityData is corrupt
|
||||
return;
|
||||
}
|
||||
|
||||
std::map<QUuid, EntityItemPointer>::iterator itr = _entities.find(entityID);
|
||||
if (itr == _entities.end()) {
|
||||
// this is an ADD
|
||||
entity = EntityTypes::constructEntityItem(entityID, properties);
|
||||
if (entity) {
|
||||
OctreePacketData packetData(false, AvatarTraits::MAXIMUM_TRAIT_SIZE);
|
||||
EncodeBitstreamParams params;
|
||||
EntityTreeElementExtraEncodeDataPointer extra { nullptr };
|
||||
OctreeElement::AppendState appendState = entity->appendEntityData(&packetData, params, extra);
|
||||
|
||||
if (appendState == OctreeElement::COMPLETED) {
|
||||
_entities[entityID] = entity;
|
||||
QByteArray tempArray((const char*)packetData.getUncompressedData(), packetData.getUncompressedSize());
|
||||
storeAvatarEntityDataPayload(entityID, tempArray);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// this is an UPDATE
|
||||
entity = itr->second;
|
||||
bool somethingChanged = entity->setProperties(properties);
|
||||
if (somethingChanged) {
|
||||
OctreePacketData packetData(false, AvatarTraits::MAXIMUM_TRAIT_SIZE);
|
||||
EncodeBitstreamParams params;
|
||||
EntityTreeElementExtraEncodeDataPointer extra { nullptr };
|
||||
OctreeElement::AppendState appendState = entity->appendEntityData(&packetData, params, extra);
|
||||
|
||||
if (appendState == OctreeElement::COMPLETED) {
|
||||
QByteArray tempArray((const char*)packetData.getUncompressedData(), packetData.getUncompressedSize());
|
||||
storeAvatarEntityDataPayload(entityID, tempArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
#include <AnimSkeleton.h>
|
||||
#include <AvatarData.h>
|
||||
#include <ScriptEngine.h>
|
||||
#include <EntityItem.h>
|
||||
|
||||
/**jsdoc
|
||||
* The <code>Avatar</code> API is used to manipulate scriptable avatars on the domain. This API is a subset of the
|
||||
|
@ -185,6 +186,26 @@ public:
|
|||
void setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement);
|
||||
bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); }
|
||||
|
||||
/**jsdoc
|
||||
* Potentially Very Expensive. Do not use.
|
||||
* @function Avatar.getAvatarEntityData
|
||||
* @returns {object}
|
||||
*/
|
||||
Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const override;
|
||||
|
||||
/**jsdoc
|
||||
* @function MyAvatar.setAvatarEntityData
|
||||
* @param {object} avatarEntityData
|
||||
*/
|
||||
Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override;
|
||||
|
||||
/**jsdoc
|
||||
* @function MyAvatar.updateAvatarEntity
|
||||
* @param {Uuid} entityID
|
||||
* @param {string} entityData
|
||||
*/
|
||||
Q_INVOKABLE void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) override;
|
||||
|
||||
public slots:
|
||||
void update(float deltatime);
|
||||
|
||||
|
@ -202,6 +223,8 @@ private:
|
|||
QHash<QString, int> _fstJointIndices; ///< 1-based, since zero is returned for missing keys
|
||||
QStringList _fstJointNames; ///< in order of depth-first traversal
|
||||
QUrl _skeletonFBXURL;
|
||||
mutable QScriptEngine _scriptEngine;
|
||||
std::map<QUuid, EntityItemPointer> _entities;
|
||||
|
||||
/// Loads the joint indices, names from the FST file (if any)
|
||||
void updateJointMappings();
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
#include <EntityEditFilters.h>
|
||||
#include <NetworkingConstants.h>
|
||||
#include <AddressManager.h>
|
||||
#include <hfm/ModelFormatRegistry.h>
|
||||
|
||||
#include "../AssignmentDynamicFactory.h"
|
||||
#include "AssignmentParentFinder.h"
|
||||
|
@ -45,6 +46,8 @@ EntityServer::EntityServer(ReceivedMessage& message) :
|
|||
|
||||
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
|
||||
DependencyManager::set<AssignmentDynamicFactory>();
|
||||
DependencyManager::set<ModelFormatRegistry>(); // ModelFormatRegistry must be defined before ModelCache. See the ModelCache ctor
|
||||
DependencyManager::set<ModelCache>();
|
||||
|
||||
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
|
||||
packetReceiver.registerListenerForTypes({ PacketType::EntityAdd,
|
||||
|
|
|
@ -75,8 +75,8 @@ void MessagesMixer::sendStatsPacket() {
|
|||
DependencyManager::get<NodeList>()->eachNode([&](const SharedNodePointer& node) {
|
||||
QJsonObject clientStats;
|
||||
clientStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuidStringWithoutCurlyBraces(node->getUUID());
|
||||
clientStats["outbound_kbps"] = node->getOutboundBandwidth();
|
||||
clientStats["inbound_kbps"] = node->getInboundBandwidth();
|
||||
clientStats["outbound_kbps"] = node->getOutboundKbps();
|
||||
clientStats["inbound_kbps"] = node->getInboundKbps();
|
||||
messagesMixerObject[uuidStringWithoutCurlyBraces(node->getUUID())] = clientStats;
|
||||
});
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
Before (image error) Size: 37 KiB After (image error) Size: 40 KiB |
Binary file not shown.
Binary file not shown.
Before (image error) Size: 51 KiB After (image error) Size: 39 KiB |
|
@ -123,7 +123,16 @@
|
|||
|
||||
{ "from": { "makeAxis" : ["Keyboard.MouseMoveLeft", "Keyboard.MouseMoveRight"] },
|
||||
"when": "Keyboard.RightMouseButton",
|
||||
"to": "Actions.Yaw",
|
||||
"to": "Actions.DeltaYaw",
|
||||
"filters":
|
||||
[
|
||||
{ "type": "scale", "scale": 0.6 }
|
||||
]
|
||||
},
|
||||
|
||||
{ "from": { "makeAxis" : ["Keyboard.MouseMoveUp", "Keyboard.MouseMoveDown"] },
|
||||
"when": "Keyboard.RightMouseButton",
|
||||
"to": "Actions.DeltaPitch",
|
||||
"filters":
|
||||
[
|
||||
{ "type": "scale", "scale": 0.6 }
|
||||
|
@ -144,20 +153,6 @@
|
|||
{ "from": "Keyboard.PgDown", "to": "Actions.VERTICAL_DOWN" },
|
||||
{ "from": "Keyboard.PgUp", "to": "Actions.VERTICAL_UP" },
|
||||
|
||||
{ "from": "Keyboard.MouseMoveUp", "when": "Keyboard.RightMouseButton", "to": "Actions.PITCH_UP",
|
||||
"filters":
|
||||
[
|
||||
{ "type": "scale", "scale": 0.6 }
|
||||
]
|
||||
|
||||
},
|
||||
{ "from": "Keyboard.MouseMoveDown", "when": "Keyboard.RightMouseButton", "to": "Actions.PITCH_DOWN",
|
||||
"filters":
|
||||
[
|
||||
{ "type": "scale", "scale": 0.6 }
|
||||
]
|
||||
},
|
||||
|
||||
{ "from": "Keyboard.TouchpadDown", "to": "Actions.PITCH_DOWN" },
|
||||
{ "from": "Keyboard.TouchpadUp", "to": "Actions.PITCH_UP" },
|
||||
|
||||
|
|
Binary file not shown.
4
interface/resources/icons/checkmark-stroke.svg
Normal file
4
interface/resources/icons/checkmark-stroke.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="149" height="150" viewBox="0 0 149 150" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M74.3055 0C115.543 0 149 33.5916 149 74.6047C149 116.008 115.543 149.6 74.3055 149.6C33.4569 149.6 0 116.008 0 74.6047C0 33.5916 33.4569 0 74.3055 0ZM74.3055 139.054C109.708 139.054 138.496 110.149 138.496 74.6047C138.496 39.4507 109.708 10.5462 74.3055 10.5462C39.2924 10.5462 10.5039 39.4507 10.5039 74.6047C10.5039 110.149 39.2924 139.054 74.3055 139.054Z" fill="#1FC6A6"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M65.3575 89.8376L106.595 43.3562C110.874 38.2784 119.044 45.3092 114.376 50.387L70.0259 100.384C68.0807 102.727 64.9684 102.727 63.0233 101.165L35.0128 78.9008C29.9554 74.6042 36.569 66.4016 41.6264 70.6982L65.3575 89.8376Z" fill="#1FC6A6"/>
|
||||
</svg>
|
After (image error) Size: 824 B |
BIN
interface/resources/icons/loader-snake-256-wf.gif
Normal file
BIN
interface/resources/icons/loader-snake-256-wf.gif
Normal file
Binary file not shown.
After ![]() (image error) Size: 27 KiB |
BIN
interface/resources/icons/loader-snake-256.gif
Normal file
BIN
interface/resources/icons/loader-snake-256.gif
Normal file
Binary file not shown.
After ![]() (image error) Size: 26 KiB |
BIN
interface/resources/images/loader-snake-128.png
Normal file
BIN
interface/resources/images/loader-snake-128.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 463 B |
|
@ -192,13 +192,13 @@ Item {
|
|||
}
|
||||
StatText {
|
||||
visible: root.expanded;
|
||||
text: "Audio In Audio: " + root.audioAudioInboundPPS + " pps, " +
|
||||
"Silent: " + root.audioSilentInboundPPS + " pps";
|
||||
text: "Audio Mixer Out: " + root.audioMixerOutKbps + " kbps, " +
|
||||
root.audioMixerOutPps + "pps";
|
||||
}
|
||||
StatText {
|
||||
visible: root.expanded;
|
||||
text: "Audio Mixer Out: " + root.audioMixerOutKbps + " kbps, " +
|
||||
root.audioMixerOutPps + "pps";
|
||||
text: "Audio In Audio: " + root.audioAudioInboundPPS + " pps, " +
|
||||
"Silent: " + root.audioSilentInboundPPS + " pps";
|
||||
}
|
||||
StatText {
|
||||
visible: root.expanded;
|
||||
|
|
|
@ -210,13 +210,13 @@ Item {
|
|||
}
|
||||
StatText {
|
||||
visible: root.expanded;
|
||||
text: "Audio In Audio: " + root.audioAudioInboundPPS + " pps, " +
|
||||
"Silent: " + root.audioSilentInboundPPS + " pps";
|
||||
text: "Audio Mixer Out: " + root.audioMixerOutKbps + " kbps, " +
|
||||
root.audioMixerOutPps + "pps";
|
||||
}
|
||||
StatText {
|
||||
visible: root.expanded;
|
||||
text: "Audio Mixer Out: " + root.audioMixerOutKbps + " kbps, " +
|
||||
root.audioMixerOutPps + "pps";
|
||||
text: "Audio In Audio: " + root.audioAudioInboundPPS + " pps, " +
|
||||
"Silent: " + root.audioSilentInboundPPS + " pps";
|
||||
}
|
||||
StatText {
|
||||
visible: root.expanded;
|
||||
|
|
|
@ -32,6 +32,10 @@ Original.Button {
|
|||
width: hifi.dimensions.buttonWidth
|
||||
height: hifi.dimensions.controlLineHeight
|
||||
|
||||
property size implicitPadding: Qt.size(20, 16)
|
||||
property int implicitWidth: buttonContentItem.implicitWidth + implicitPadding.width
|
||||
property int implicitHeight: buttonContentItem.implicitHeight + implicitPadding.height
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
onHoveredChanged: {
|
||||
|
@ -94,6 +98,8 @@ Original.Button {
|
|||
|
||||
contentItem: Item {
|
||||
id: buttonContentItem
|
||||
implicitWidth: (buttonGlyph.visible ? buttonGlyph.implicitWidth : 0) + buttonText.implicitWidth
|
||||
implicitHeight: buttonText.implicitHeight
|
||||
TextMetrics {
|
||||
id: buttonGlyphTextMetrics;
|
||||
font: buttonGlyph.font;
|
||||
|
|
|
@ -10,7 +10,7 @@ import "avatarapp"
|
|||
Rectangle {
|
||||
id: root
|
||||
width: 480
|
||||
height: 706
|
||||
height: 706
|
||||
|
||||
property bool keyboardEnabled: true
|
||||
property bool keyboardRaised: false
|
||||
|
@ -254,7 +254,8 @@ Rectangle {
|
|||
onSaveClicked: function() {
|
||||
var avatarSettings = {
|
||||
dominantHand : settings.dominantHandIsLeft ? 'left' : 'right',
|
||||
collisionsEnabled : settings.avatarCollisionsOn,
|
||||
collisionsEnabled : settings.environmentCollisionsOn,
|
||||
otherAvatarsCollisionsEnabled : settings.otherAvatarsCollisionsOn,
|
||||
animGraphOverrideUrl : settings.avatarAnimationOverrideJSON,
|
||||
collisionSoundUrl : settings.avatarCollisionSoundUrl
|
||||
};
|
||||
|
@ -415,7 +416,7 @@ Rectangle {
|
|||
width: 21.2
|
||||
height: 19.3
|
||||
source: isAvatarInFavorites ? '../../images/FavoriteIconActive.svg' : '../../images/FavoriteIconInActive.svg'
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// TextStyle5
|
||||
|
@ -424,7 +425,7 @@ Rectangle {
|
|||
Layout.fillWidth: true
|
||||
text: isAvatarInFavorites ? avatarName : "Add to Favorites"
|
||||
elide: Qt.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
|
|
24
interface/resources/qml/hifi/AvatarPackagerWindow.qml
Normal file
24
interface/resources/qml/hifi/AvatarPackagerWindow.qml
Normal file
|
@ -0,0 +1,24 @@
|
|||
import QtQuick 2.6
|
||||
import "../stylesUit" 1.0
|
||||
import "../windows" as Windows
|
||||
import "avatarPackager" 1.0
|
||||
|
||||
Windows.ScrollingWindow {
|
||||
id: root
|
||||
objectName: "AvatarPackager"
|
||||
width: 480
|
||||
height: 706
|
||||
title: "Avatar Packager"
|
||||
resizable: false
|
||||
opacity: parent.opacity
|
||||
destroyOnHidden: true
|
||||
implicitWidth: 384; implicitHeight: 640
|
||||
minSize: Qt.vector2d(480, 706)
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
AvatarPackagerApp {
|
||||
height: pane.height
|
||||
width: pane.width
|
||||
}
|
||||
}
|
|
@ -0,0 +1,396 @@
|
|||
import QtQuick 2.6
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQml.Models 2.1
|
||||
import QtGraphicalEffects 1.0
|
||||
import Hifi.AvatarPackager.AvatarProjectStatus 1.0
|
||||
import "../../controlsUit" 1.0 as HifiControls
|
||||
import "../../stylesUit" 1.0
|
||||
import "../../controls" 1.0
|
||||
import "../../dialogs"
|
||||
import "../avatarapp" 1.0 as AvatarApp
|
||||
|
||||
Item {
|
||||
id: windowContent
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
property alias desktopObject: avatarPackager.desktopObject
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: {
|
||||
unfocusser.forceActiveFocus();
|
||||
}
|
||||
Item {
|
||||
id: unfocusser
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
|
||||
InfoBox {
|
||||
id: fileListPopup
|
||||
|
||||
title: "List of Files"
|
||||
|
||||
content: Rectangle {
|
||||
id: fileList
|
||||
|
||||
color: "#404040"
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 10
|
||||
anchors.bottomMargin: 10
|
||||
anchors.leftMargin: 29
|
||||
anchors.rightMargin: 29
|
||||
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
anchors.fill: parent
|
||||
model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles
|
||||
delegate: Rectangle {
|
||||
width: parent.width
|
||||
height: fileText.implicitHeight + 8
|
||||
color: "#404040"
|
||||
RalewaySemiBold {
|
||||
id: fileText
|
||||
size: 16
|
||||
elide: Text.ElideLeft
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.topMargin: 4
|
||||
width: parent.width - 10
|
||||
color: "white"
|
||||
text: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InfoBox {
|
||||
id: errorPopup
|
||||
|
||||
property string errorMessage
|
||||
|
||||
boxWidth: 380
|
||||
boxHeight: 293
|
||||
|
||||
content: RalewayRegular {
|
||||
|
||||
id: bodyMessage
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: 10
|
||||
anchors.leftMargin: 29
|
||||
anchors.rightMargin: 29
|
||||
|
||||
size: 20
|
||||
color: "white"
|
||||
text: errorPopup.errorMessage
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
function show(title, message) {
|
||||
errorPopup.title = title;
|
||||
errorMessage = message;
|
||||
errorPopup.open();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: modalOverlay
|
||||
anchors.fill: parent
|
||||
z: 20
|
||||
color: "#a15d5d5d"
|
||||
visible: false
|
||||
|
||||
// This mouse area captures the cursor events while the modalOverlay is active
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
propagateComposedEvents: false
|
||||
hoverEnabled: true
|
||||
}
|
||||
}
|
||||
|
||||
AvatarApp.MessageBox {
|
||||
id: popup
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
closeOnClickOutside: true
|
||||
}
|
||||
|
||||
Column {
|
||||
id: avatarPackager
|
||||
anchors.fill: parent
|
||||
state: "main"
|
||||
states: [
|
||||
State {
|
||||
name: AvatarPackagerState.main
|
||||
PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); docsEnabled: true; backButtonVisible: false }
|
||||
PropertyChanges { target: avatarPackagerMain; visible: true }
|
||||
PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer }
|
||||
},
|
||||
State {
|
||||
name: AvatarPackagerState.createProject
|
||||
PropertyChanges { target: avatarPackagerHeader; title: qsTr("Create Project") }
|
||||
PropertyChanges { target: createAvatarProject; visible: true }
|
||||
PropertyChanges { target: avatarPackagerFooter; content: createAvatarProject.footer }
|
||||
},
|
||||
State {
|
||||
name: AvatarPackagerState.project
|
||||
PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; canRename: true }
|
||||
PropertyChanges { target: avatarProject; visible: true }
|
||||
PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer }
|
||||
},
|
||||
State {
|
||||
name: AvatarPackagerState.projectUpload
|
||||
PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; backButtonEnabled: false }
|
||||
PropertyChanges { target: avatarUploader; visible: true }
|
||||
PropertyChanges { target: avatarPackagerFooter; visible: false }
|
||||
}
|
||||
]
|
||||
|
||||
property alias showModalOverlay: modalOverlay.visible
|
||||
|
||||
property var desktopObject: desktop
|
||||
|
||||
function openProject(path) {
|
||||
let status = AvatarPackagerCore.openAvatarProject(path);
|
||||
if (status !== AvatarProjectStatus.SUCCESS) {
|
||||
displayErrorMessage(status);
|
||||
return status;
|
||||
}
|
||||
avatarProject.reset();
|
||||
avatarPackager.state = AvatarPackagerState.project;
|
||||
return status;
|
||||
}
|
||||
|
||||
function displayErrorMessage(status) {
|
||||
if (status === AvatarProjectStatus.SUCCESS) {
|
||||
return;
|
||||
}
|
||||
switch (status) {
|
||||
case AvatarProjectStatus.ERROR_CREATE_PROJECT_NAME:
|
||||
errorPopup.show("Project Folder Already Exists", "A folder with that name already exists at that location. Please choose a different project name or location.");
|
||||
break;
|
||||
case AvatarProjectStatus.ERROR_CREATE_CREATING_DIRECTORIES:
|
||||
errorPopup.show("Project Folders Creation Error", "There was a problem creating the Avatar Project directory. Please check the project location and try again.");
|
||||
break;
|
||||
case AvatarProjectStatus.ERROR_CREATE_FIND_MODEL:
|
||||
errorPopup.show("Cannot Find Model File", "There was a problem while trying to find the specified model file. Please verify that it exists at the specified location.");
|
||||
break;
|
||||
case AvatarProjectStatus.ERROR_CREATE_OPEN_MODEL:
|
||||
errorPopup.show("Cannot Open Model File", "There was a problem while trying to open the specified model file.");
|
||||
break;
|
||||
case AvatarProjectStatus.ERROR_CREATE_READ_MODEL:
|
||||
errorPopup.show("Error Read Model File", "There was a problem while trying to read the specified model file. Please check that the file is a valid FBX file and try again.");
|
||||
break;
|
||||
case AvatarProjectStatus.ERROR_CREATE_WRITE_FST:
|
||||
errorPopup.show("Error Writing Project File", "There was a problem while trying to write the FST file.");
|
||||
break;
|
||||
case AvatarProjectStatus.ERROR_OPEN_INVALID_FILE_TYPE:
|
||||
errorPopup.show("Invalid Project Path", "The avatar packager can only open FST files.");
|
||||
break;
|
||||
case AvatarProjectStatus.ERROR_OPEN_PROJECT_FOLDER:
|
||||
errorPopup.show("Project Missing", "Project folder cannot be found. Please locate the folder and copy/move it to its original location.");
|
||||
break;
|
||||
case AvatarProjectStatus.ERROR_OPEN_FIND_FST:
|
||||
errorPopup.show("File Missing", "We cannot find the project file (.fst) in the project folder. Please locate it and move it to the project folder.");
|
||||
break;
|
||||
case AvatarProjectStatus.ERROR_OPEN_OPEN_FST:
|
||||
errorPopup.show("File Read Error", "We cannot read the project file (.fst).");
|
||||
break;
|
||||
case AvatarProjectStatus.ERROR_OPEN_FIND_MODEL:
|
||||
errorPopup.show("File Missing", "We cannot find the avatar model file (.fbx) in the project folder. Please locate it and move it to the project folder.");
|
||||
break;
|
||||
default:
|
||||
errorPopup.show("Error Message Missing", "Error message missing for status " + status);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function openDocs() {
|
||||
Qt.openUrlExternally("https://docs.highfidelity.com/create/avatars/create-avatars#how-to-package-your-avatar");
|
||||
}
|
||||
|
||||
AvatarPackagerHeader {
|
||||
z: 100
|
||||
|
||||
id: avatarPackagerHeader
|
||||
colorScheme: root.colorScheme
|
||||
onBackButtonClicked: {
|
||||
avatarPackager.state = AvatarPackagerState.main;
|
||||
}
|
||||
onDocsButtonClicked: {
|
||||
avatarPackager.openDocs();
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
height: windowContent.height - avatarPackagerHeader.height - avatarPackagerFooter.height
|
||||
width: windowContent.width
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#404040"
|
||||
}
|
||||
|
||||
AvatarProject {
|
||||
id: avatarProject
|
||||
colorScheme: root.colorScheme
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
AvatarProjectUpload {
|
||||
id: avatarUploader
|
||||
anchors.fill: parent
|
||||
root: avatarProject
|
||||
}
|
||||
|
||||
CreateAvatarProject {
|
||||
id: createAvatarProject
|
||||
colorScheme: root.colorScheme
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Item {
|
||||
id: avatarPackagerMain
|
||||
visible: false
|
||||
anchors.fill: parent
|
||||
|
||||
property var footer: Item {
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 17
|
||||
HifiControls.Button {
|
||||
id: createProjectButton
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: openProjectButton.left
|
||||
anchors.rightMargin: 22
|
||||
height: 40
|
||||
width: 134
|
||||
text: qsTr("New Project")
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
createAvatarProject.clearInputs();
|
||||
avatarPackager.state = AvatarPackagerState.createProject;
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
id: openProjectButton
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
height: 40
|
||||
width: 133
|
||||
text: qsTr("Open Project")
|
||||
color: hifi.buttons.blue
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
avatarPackager.showModalOverlay = true;
|
||||
|
||||
let browser = avatarPackager.desktopObject.fileDialog({
|
||||
selectDirectory: false,
|
||||
dir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH),
|
||||
filter: "Avatar Project FST Files (*.fst)",
|
||||
title: "Open Project (.fst)",
|
||||
});
|
||||
|
||||
browser.canceled.connect(function() {
|
||||
avatarPackager.showModalOverlay = false;
|
||||
});
|
||||
|
||||
browser.selectedFile.connect(function(fileUrl) {
|
||||
let fstFilePath = fileDialogHelper.urlToPath(fileUrl);
|
||||
avatarPackager.showModalOverlay = false;
|
||||
avatarPackager.openProject(fstFilePath);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
visible: AvatarPackagerCore.recentProjects.length === 0
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: 18
|
||||
leftMargin: 16
|
||||
rightMargin: 16
|
||||
}
|
||||
RalewayRegular {
|
||||
size: 20
|
||||
color: "white"
|
||||
text: "Use a custom avatar of your choice."
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
RalewayRegular {
|
||||
size: 20
|
||||
color: "white"
|
||||
text: "<a href='javascript:void'>Visit our docs</a> to learn more about using the packager."
|
||||
linkColor: "#00B4EF"
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
onLinkActivated: {
|
||||
avatarPackager.openDocs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
visible: AvatarPackagerCore.recentProjects.length > 0
|
||||
|
||||
RalewayRegular {
|
||||
id: recentProjectsText
|
||||
|
||||
color: 'white'
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.topMargin: 16
|
||||
anchors.leftMargin: 16
|
||||
|
||||
size: 20
|
||||
|
||||
text: "Recent Projects"
|
||||
|
||||
onLinkActivated: fileListPopup.open()
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
top: recentProjectsText.bottom
|
||||
topMargin: 16
|
||||
leftMargin: 16
|
||||
rightMargin: 16
|
||||
}
|
||||
spacing: 10
|
||||
|
||||
Repeater {
|
||||
model: AvatarPackagerCore.recentProjects
|
||||
AvatarProjectCard {
|
||||
title: modelData.name
|
||||
path: modelData.projectPath
|
||||
onOpen: avatarPackager.openProject(modelData.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AvatarPackagerFooter {
|
||||
id: avatarPackagerFooter
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import QtQuick 2.6
|
||||
|
||||
import "../../controlsUit" 1.0 as HifiControls
|
||||
import "../../stylesUit" 1.0
|
||||
|
||||
Rectangle {
|
||||
id: avatarPackagerFooter
|
||||
|
||||
color: "#404040"
|
||||
height: content === defaultContent ? 0 : 74
|
||||
visible: content !== defaultContent
|
||||
width: parent.width
|
||||
|
||||
property var content: Item { id: defaultContent }
|
||||
|
||||
children: [background, content]
|
||||
|
||||
property var background: Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#404040"
|
||||
|
||||
Rectangle {
|
||||
id: topBorder1
|
||||
|
||||
anchors.top: parent.top
|
||||
|
||||
color: "#252525"
|
||||
height: 1
|
||||
width: parent.width
|
||||
}
|
||||
Rectangle {
|
||||
id: topBorder2
|
||||
|
||||
anchors.top: topBorder1.bottom
|
||||
|
||||
color: "#575757"
|
||||
height: 1
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
import QtQuick 2.6
|
||||
|
||||
import "../../controlsUit" 1.0 as HifiControls
|
||||
import "../../stylesUit" 1.0
|
||||
import "../avatarapp" 1.0
|
||||
|
||||
ShadowRectangle {
|
||||
id: root
|
||||
|
||||
width: parent.width
|
||||
height: 74
|
||||
color: "#252525"
|
||||
|
||||
property string title: qsTr("Avatar Packager")
|
||||
property alias docsEnabled: docs.visible
|
||||
property bool backButtonVisible: true // If false, is not visible and does not take up space
|
||||
property bool backButtonEnabled: true // If false, is not visible but does not affect space
|
||||
property bool canRename: false
|
||||
property int colorScheme
|
||||
|
||||
property color textColor: "white"
|
||||
property color hoverTextColor: "gray"
|
||||
property color pressedTextColor: "#6A6A6A"
|
||||
|
||||
signal backButtonClicked
|
||||
signal docsButtonClicked
|
||||
|
||||
RalewayButton {
|
||||
id: back
|
||||
|
||||
visible: backButtonEnabled && backButtonVisible
|
||||
|
||||
size: 28
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 16
|
||||
|
||||
text: "◀"
|
||||
|
||||
onClicked: root.backButtonClicked()
|
||||
}
|
||||
Item {
|
||||
id: titleArea
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: root.backButtonVisible ? back.right : parent.left
|
||||
anchors.leftMargin: root.backButtonVisible ? 11 : 21
|
||||
anchors.right: docs.left
|
||||
states: [
|
||||
State {
|
||||
name: "renaming"
|
||||
PropertyChanges { target: title; visible: false }
|
||||
PropertyChanges { target: titleInputArea; visible: true }
|
||||
}
|
||||
]
|
||||
|
||||
Item {
|
||||
id: title
|
||||
anchors.fill: parent
|
||||
|
||||
RalewaySemiBold {
|
||||
id: titleNotRenameable
|
||||
|
||||
visible: !root.canRename
|
||||
|
||||
size: 28
|
||||
anchors.fill: parent
|
||||
text: root.title
|
||||
color: "white"
|
||||
}
|
||||
|
||||
RalewayButton {
|
||||
id: titleRenameable
|
||||
|
||||
visible: root.canRename
|
||||
enabled: root.canRename
|
||||
|
||||
size: 28
|
||||
anchors.fill: parent
|
||||
text: root.title
|
||||
|
||||
onClicked: {
|
||||
if (!root.canRename || AvatarPackagerCore.currentAvatarProject === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
titleArea.state = "renaming";
|
||||
titleInput.text = AvatarPackagerCore.currentAvatarProject.name;
|
||||
titleInput.selectAll();
|
||||
titleInput.forceActiveFocus(Qt.MouseFocusReason);
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
id: titleInputArea
|
||||
visible: false
|
||||
anchors.fill: parent
|
||||
|
||||
HifiControls.TextField {
|
||||
id: titleInput
|
||||
anchors.fill: parent
|
||||
text: ""
|
||||
colorScheme: root.colorScheme
|
||||
font.family: "Fira Sans"
|
||||
font.pixelSize: 28
|
||||
z: 200
|
||||
onFocusChanged: {
|
||||
if (titleArea.state === "renaming" && !focus) {
|
||||
accepted();
|
||||
}
|
||||
}
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
titleArea.state = "";
|
||||
}
|
||||
}
|
||||
onAccepted: {
|
||||
if (acceptableInput) {
|
||||
AvatarPackagerCore.currentAvatarProject.name = text;
|
||||
}
|
||||
titleArea.state = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RalewayButton {
|
||||
id: docs
|
||||
visible: false
|
||||
size: 28
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 16
|
||||
|
||||
text: qsTr("Docs")
|
||||
|
||||
onClicked: {
|
||||
docsButtonClicked();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
pragma Singleton
|
||||
import QtQuick 2.6
|
||||
|
||||
Item {
|
||||
id: singleton
|
||||
readonly property string main: "main"
|
||||
readonly property string project: "project"
|
||||
readonly property string createProject: "createProject"
|
||||
readonly property string projectUpload: "projectUpload"
|
||||
}
|
336
interface/resources/qml/hifi/avatarPackager/AvatarProject.qml
Normal file
336
interface/resources/qml/hifi/avatarPackager/AvatarProject.qml
Normal file
|
@ -0,0 +1,336 @@
|
|||
import QtQuick 2.6
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtGraphicalEffects 1.0
|
||||
|
||||
import QtQuick.Controls 2.2 as Original
|
||||
|
||||
import "../../controlsUit" 1.0 as HifiControls
|
||||
import "../../stylesUit" 1.0
|
||||
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
Style { id: style }
|
||||
|
||||
property int colorScheme
|
||||
property var uploader: null
|
||||
|
||||
property bool hasSuccessfullyUploaded: true
|
||||
|
||||
visible: false
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
|
||||
function reset() {
|
||||
hasSuccessfullyUploaded = false;
|
||||
uploader = null;
|
||||
}
|
||||
|
||||
property var footer: Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Item {
|
||||
id: uploadFooter
|
||||
|
||||
visible: !root.uploader || root.finished || root.uploader.state !== 4
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 17
|
||||
|
||||
HifiControls.Button {
|
||||
id: uploadButton
|
||||
|
||||
visible: AvatarPackagerCore.currentAvatarProject && !AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded
|
||||
enabled: Account.loggedIn
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
|
||||
text: qsTr("Upload")
|
||||
color: hifi.buttons.blue
|
||||
colorScheme: root.colorScheme
|
||||
width: 133
|
||||
height: 40
|
||||
onClicked: {
|
||||
uploadNew();
|
||||
}
|
||||
}
|
||||
HifiControls.Button {
|
||||
id: updateButton
|
||||
|
||||
visible: AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded
|
||||
enabled: Account.loggedIn
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
|
||||
text: qsTr("Update")
|
||||
color: hifi.buttons.blue
|
||||
colorScheme: root.colorScheme
|
||||
width: 134
|
||||
height: 40
|
||||
onClicked: {
|
||||
showConfirmUploadPopup(uploadNew, uploadUpdate);
|
||||
}
|
||||
}
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: root.hasSuccessfullyUploaded
|
||||
|
||||
HifiControls.Button {
|
||||
enabled: Account.loggedIn
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: viewInInventoryButton.left
|
||||
anchors.rightMargin: 16
|
||||
|
||||
text: qsTr("Update")
|
||||
color: hifi.buttons.white
|
||||
colorScheme: root.colorScheme
|
||||
width: 134
|
||||
height: 40
|
||||
onClicked: {
|
||||
showConfirmUploadPopup(uploadNew, uploadUpdate);
|
||||
}
|
||||
}
|
||||
HifiControls.Button {
|
||||
id: viewInInventoryButton
|
||||
|
||||
enabled: Account.loggedIn
|
||||
|
||||
width: 168
|
||||
height: 40
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
|
||||
text: qsTr("View in Inventory")
|
||||
color: hifi.buttons.blue
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
onClicked: AvatarPackagerCore.currentAvatarProject.openInInventory()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: uploadingItemFooter
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 1
|
||||
visible: !!root.uploader && !root.finished && root.uploader.state === 4
|
||||
|
||||
color: "#00B4EF"
|
||||
|
||||
LoadingCircle {
|
||||
id: runningImage
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 16
|
||||
|
||||
width: 28
|
||||
height: 28
|
||||
|
||||
white: true
|
||||
}
|
||||
RalewayRegular {
|
||||
id: stepText
|
||||
|
||||
size: 20
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: runningImage.right
|
||||
anchors.leftMargin: 16
|
||||
|
||||
text: "Adding item to Inventory"
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function uploadNew() {
|
||||
upload(false);
|
||||
}
|
||||
function uploadUpdate() {
|
||||
upload(true);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.uploader
|
||||
onStateChanged: {
|
||||
root.hasSuccessfullyUploaded = newState >= 4;
|
||||
}
|
||||
}
|
||||
|
||||
function upload(updateExisting) {
|
||||
root.uploader = AvatarPackagerCore.currentAvatarProject.upload(updateExisting);
|
||||
console.log("uploader: "+ root.uploader);
|
||||
root.uploader.send();
|
||||
avatarPackager.state = AvatarPackagerState.projectUpload;
|
||||
}
|
||||
|
||||
function showConfirmUploadPopup() {
|
||||
popup.titleText = 'Overwrite Avatar';
|
||||
popup.bodyText = 'You have previously uploaded the avatar file from this project.' +
|
||||
' This will overwrite that avatar and you won’t be able to access the older version.';
|
||||
|
||||
popup.button1text = 'CREATE NEW';
|
||||
popup.button2text = 'OVERWRITE';
|
||||
|
||||
popup.onButton2Clicked = function() {
|
||||
popup.close();
|
||||
uploadUpdate();
|
||||
};
|
||||
popup.onButton1Clicked = function() {
|
||||
popup.close();
|
||||
showConfirmCreateNewPopup();
|
||||
};
|
||||
|
||||
popup.open();
|
||||
}
|
||||
|
||||
function showConfirmCreateNewPopup(confirmCallback) {
|
||||
popup.titleText = 'Create New';
|
||||
popup.bodyText = 'This will upload your current files with the same avatar name.' +
|
||||
' You will lose the ability to update the previously uploaded avatar. Are you sure you want to continue?';
|
||||
|
||||
popup.button1text = 'CANCEL';
|
||||
popup.button2text = 'CONFIRM';
|
||||
|
||||
popup.onButton1Clicked = function() {
|
||||
popup.close()
|
||||
};
|
||||
popup.onButton2Clicked = function() {
|
||||
popup.close();
|
||||
uploadNew();
|
||||
};
|
||||
|
||||
popup.open();
|
||||
}
|
||||
|
||||
RalewayRegular {
|
||||
id: infoMessage
|
||||
|
||||
states: [
|
||||
State {
|
||||
when: root.hasSuccessfullyUploaded
|
||||
name: "upload-success"
|
||||
PropertyChanges {
|
||||
target: infoMessage
|
||||
text: "Your avatar has been successfully uploaded to our servers. Make changes to your avatar by editing and uploading the project files."
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "has-previous-success"
|
||||
when: !!AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID
|
||||
PropertyChanges {
|
||||
target: infoMessage
|
||||
text: "Click \"Update\" to overwrite the hosted files and update the avatar in your inventory. You will have to “Wear” the avatar again to see changes."
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
color: 'white'
|
||||
size: 20
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
anchors.bottomMargin: 24
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
text: "You can upload your files to our servers to always access them, and to make your avatar visible to other users."
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
id: openFolderButton
|
||||
|
||||
visible: false
|
||||
width: parent.width
|
||||
anchors.top: infoMessage.bottom
|
||||
anchors.topMargin: 10
|
||||
text: qsTr("Open Project Folder")
|
||||
colorScheme: root.colorScheme
|
||||
height: 30
|
||||
onClicked: {
|
||||
fileDialogHelper.openDirectory(fileDialogHelper.pathToUrl(AvatarPackagerCore.currentAvatarProject.projectFolderPath));
|
||||
}
|
||||
}
|
||||
|
||||
RalewayRegular {
|
||||
id: showFilesText
|
||||
|
||||
color: 'white'
|
||||
linkColor: style.colors.blueHighlight
|
||||
|
||||
visible: AvatarPackagerCore.currentAvatarProject !== null
|
||||
|
||||
anchors.bottom: loginRequiredMessage.top
|
||||
anchors.bottomMargin: 10
|
||||
|
||||
size: 20
|
||||
|
||||
text: AvatarPackagerCore.currentAvatarProject ? AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in project. <a href='toggle'>See list</a>" : ""
|
||||
|
||||
onLinkActivated: fileListPopup.open()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: loginRequiredMessage
|
||||
|
||||
visible: !Account.loggedIn
|
||||
height: !Account.loggedIn ? loginRequiredTextRow.height + 20 : 0
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
|
||||
color: "#FFD6AD"
|
||||
|
||||
border.color: "#F39622"
|
||||
border.width: 2
|
||||
radius: 2
|
||||
|
||||
Item {
|
||||
id: loginRequiredTextRow
|
||||
|
||||
height: Math.max(loginWarningGlyph.implicitHeight, loginWarningText.implicitHeight)
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
|
||||
HiFiGlyphs {
|
||||
id: loginWarningGlyph
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
|
||||
width: implicitWidth
|
||||
|
||||
size: 48
|
||||
text: "+"
|
||||
color: "black"
|
||||
}
|
||||
RalewayRegular {
|
||||
id: loginWarningText
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 16
|
||||
anchors.left: loginWarningGlyph.right
|
||||
anchors.right: parent.right
|
||||
|
||||
text: "Please login to upload your avatar to High Fidelity hosting."
|
||||
size: 18
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
import QtQuick 2.0
|
||||
import QtGraphicalEffects 1.0
|
||||
|
||||
import "../../controlsUit" 1.0 as HifiControls
|
||||
import "../../stylesUit" 1.0
|
||||
|
||||
|
||||
Item {
|
||||
id: projectCard
|
||||
height: 80
|
||||
width: parent.width
|
||||
|
||||
property alias title: title.text
|
||||
property alias path: path.text
|
||||
|
||||
property color textColor: "#E3E3E3"
|
||||
property color hoverTextColor: "#121212"
|
||||
property color pressedTextColor: "#121212"
|
||||
|
||||
property color backgroundColor: "#121212"
|
||||
property color hoverBackgroundColor: "#E3E3E3"
|
||||
property color pressedBackgroundColor: "#6A6A6A"
|
||||
|
||||
signal open
|
||||
|
||||
state: mouseArea.pressed ? "pressed" : (mouseArea.containsMouse ? "hover" : "normal")
|
||||
states: [
|
||||
State {
|
||||
name: "normal"
|
||||
PropertyChanges { target: background; color: backgroundColor }
|
||||
PropertyChanges { target: title; color: textColor }
|
||||
PropertyChanges { target: path; color: textColor }
|
||||
},
|
||||
State {
|
||||
name: "hover"
|
||||
PropertyChanges { target: background; color: hoverBackgroundColor }
|
||||
PropertyChanges { target: title; color: hoverTextColor }
|
||||
PropertyChanges { target: path; color: hoverTextColor }
|
||||
},
|
||||
State {
|
||||
name: "pressed"
|
||||
PropertyChanges { target: background; color: pressedBackgroundColor }
|
||||
PropertyChanges { target: title; color: pressedTextColor }
|
||||
PropertyChanges { target: path; color: pressedTextColor }
|
||||
}
|
||||
]
|
||||
|
||||
Rectangle {
|
||||
id: background
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
color: "#121212"
|
||||
radius: 4
|
||||
|
||||
RalewayBold {
|
||||
id: title
|
||||
elide: "ElideRight"
|
||||
anchors {
|
||||
top: parent.top
|
||||
topMargin: 13
|
||||
left: parent.left
|
||||
leftMargin: 16
|
||||
right: parent.right
|
||||
rightMargin: 16
|
||||
}
|
||||
text: "<title missing>"
|
||||
size: 24
|
||||
}
|
||||
|
||||
RalewayRegular {
|
||||
id: path
|
||||
anchors {
|
||||
top: title.bottom
|
||||
left: parent.left
|
||||
leftMargin: 32
|
||||
right: background.right
|
||||
rightMargin: 16
|
||||
}
|
||||
elide: "ElideLeft"
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: "<path missing>"
|
||||
size: 20
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: open()
|
||||
}
|
||||
}
|
||||
|
||||
DropShadow {
|
||||
id: shadow
|
||||
anchors.fill: background
|
||||
radius: 4
|
||||
horizontalOffset: 0
|
||||
verticalOffset: 4
|
||||
color: Qt.rgba(0, 0, 0, 0.25)
|
||||
source: background
|
||||
}
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
import QtQuick 2.6
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtGraphicalEffects 1.0
|
||||
|
||||
import QtQuick.Controls 2.2 as Original
|
||||
|
||||
import "../../controlsUit" 1.0 as HifiControls
|
||||
import "../../stylesUit" 1.0
|
||||
|
||||
Item {
|
||||
id: uploadingScreen
|
||||
|
||||
property var root: undefined
|
||||
visible: false
|
||||
anchors.fill: parent
|
||||
|
||||
Timer {
|
||||
id: backToProjectTimer
|
||||
interval: 2000
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (avatarPackager.state === AvatarPackagerState.projectUpload) {
|
||||
avatarPackager.state = AvatarPackagerState.project;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stateChangedCallback(newState) {
|
||||
if (newState >= 4) {
|
||||
root.uploader.stateChanged.disconnect(stateChangedCallback);
|
||||
backToProjectTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
root.uploader.stateChanged.connect(stateChangedCallback);
|
||||
root.uploader.finishedChanged.connect(function() {
|
||||
if (root.uploader.error === 0) {
|
||||
backToProjectTimer.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: uploadStatus
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Item {
|
||||
id: statusItem
|
||||
|
||||
width: parent.width
|
||||
height: 256
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "success"
|
||||
when: root.uploader.state >= 4 && root.uploader.error === 0
|
||||
PropertyChanges { target: uploadSpinner; visible: false }
|
||||
PropertyChanges { target: errorIcon; visible: false }
|
||||
PropertyChanges { target: successIcon; visible: true }
|
||||
},
|
||||
State {
|
||||
name: "error"
|
||||
when: root.uploader.finished && root.uploader.error !== 0
|
||||
PropertyChanges { target: uploadSpinner; visible: false }
|
||||
PropertyChanges { target: errorIcon; visible: true }
|
||||
PropertyChanges { target: successIcon; visible: false }
|
||||
PropertyChanges { target: errorFooter; visible: true }
|
||||
PropertyChanges { target: errorMessage; visible: true }
|
||||
}
|
||||
]
|
||||
|
||||
AnimatedImage {
|
||||
id: uploadSpinner
|
||||
|
||||
visible: true
|
||||
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
width: 164
|
||||
height: 164
|
||||
|
||||
source: "../../../icons/loader-snake-256.gif"
|
||||
playing: true
|
||||
}
|
||||
|
||||
HiFiGlyphs {
|
||||
id: errorIcon
|
||||
|
||||
visible: false
|
||||
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
size: 315
|
||||
text: "+"
|
||||
color: "#EA4C5F"
|
||||
}
|
||||
|
||||
Image {
|
||||
id: successIcon
|
||||
|
||||
visible: false
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
width: 148
|
||||
height: 148
|
||||
|
||||
source: "../../../icons/checkmark-stroke.svg"
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: statusRows
|
||||
|
||||
anchors.top: statusItem.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 12
|
||||
|
||||
AvatarUploadStatusItem {
|
||||
id: statusCategories
|
||||
uploader: root.uploader
|
||||
text: "Retrieving categories"
|
||||
|
||||
uploaderState: 1
|
||||
}
|
||||
AvatarUploadStatusItem {
|
||||
id: statusUploading
|
||||
uploader: root.uploader
|
||||
anchors.top: statusCategories.bottom
|
||||
text: "Uploading data"
|
||||
|
||||
uploaderState: 2
|
||||
}
|
||||
AvatarUploadStatusItem {
|
||||
id: statusResponse
|
||||
uploader: root.uploader
|
||||
anchors.top: statusUploading.bottom
|
||||
text: "Waiting for response"
|
||||
|
||||
uploaderState: 3
|
||||
}
|
||||
}
|
||||
|
||||
RalewayRegular {
|
||||
id: errorMessage
|
||||
|
||||
visible: false
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: errorFooter.top
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 32
|
||||
|
||||
size: 28
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
text: "We couldn't upload your avatar at this time. Please try again later."
|
||||
}
|
||||
|
||||
AvatarPackagerFooter {
|
||||
id: errorFooter
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
visible: false
|
||||
|
||||
content: Item {
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 17
|
||||
HifiControls.Button {
|
||||
id: backButton
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
|
||||
text: qsTr("Back")
|
||||
color: hifi.buttons.blue
|
||||
colorScheme: root.colorScheme
|
||||
width: 133
|
||||
height: 40
|
||||
onClicked: {
|
||||
avatarPackager.state = AvatarPackagerState.project;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
import QtQuick 2.6
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import "../../controlsUit" 1.0 as HifiControls
|
||||
import "../../stylesUit" 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
height: 48
|
||||
|
||||
property string text: "NO STEP TEXT"
|
||||
property int uploaderState;
|
||||
property var uploader;
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: ""
|
||||
when: root.uploader === null
|
||||
},
|
||||
State {
|
||||
name: "success"
|
||||
when: root.uploader.state > uploaderState
|
||||
PropertyChanges { target: stepText; color: "white" }
|
||||
PropertyChanges { target: successGlyph; visible: true }
|
||||
},
|
||||
State {
|
||||
name: "fail"
|
||||
when: root.uploader.error !== 0
|
||||
PropertyChanges { target: stepText; color: "#EA4C5F" }
|
||||
PropertyChanges { target: failGlyph; visible: true }
|
||||
},
|
||||
State {
|
||||
name: "running"
|
||||
when: root.uploader.state === uploaderState
|
||||
PropertyChanges { target: stepText; color: "white" }
|
||||
PropertyChanges { target: runningImage; visible: true; playing: true }
|
||||
}
|
||||
]
|
||||
|
||||
Item {
|
||||
id: statusItem
|
||||
|
||||
width: 48
|
||||
height: parent.height
|
||||
|
||||
LoadingCircle {
|
||||
id: runningImage
|
||||
|
||||
visible: false
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
width: 32
|
||||
height: 32
|
||||
}
|
||||
Image {
|
||||
id: successGlyph
|
||||
|
||||
visible: false
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
width: 30
|
||||
height: 30
|
||||
|
||||
source: "../../../icons/checkmark-stroke.svg"
|
||||
}
|
||||
HiFiGlyphs {
|
||||
id: failGlyph
|
||||
|
||||
visible: false
|
||||
|
||||
width: implicitWidth
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
size: 48
|
||||
text: "+"
|
||||
color: "#EA4C5F"
|
||||
}
|
||||
}
|
||||
RalewayRegular {
|
||||
id: stepText
|
||||
|
||||
anchors.left: statusItem.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
text: root.text
|
||||
size: 28
|
||||
color: "#777777"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import QtQuick 2.6
|
||||
|
||||
import "../../controlsUit" 1.0 as HifiControls
|
||||
import "../../stylesUit" 1.0
|
||||
|
||||
import TabletScriptingInterface 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property bool pressed: mouseArea.state == "pressed"
|
||||
readonly property bool hovered: mouseArea.state == "hovering"
|
||||
|
||||
signal clicked()
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
root.focus = true
|
||||
Tablet.playSound(TabletEnums.ButtonClick);
|
||||
root.clicked();
|
||||
}
|
||||
|
||||
property string lastState: ""
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: ""
|
||||
StateChangeScript {
|
||||
script: {
|
||||
mouseArea.lastState = mouseArea.state;
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "pressed"
|
||||
when: mouseArea.containsMouse && mouseArea.pressed
|
||||
StateChangeScript {
|
||||
script: {
|
||||
mouseArea.lastState = mouseArea.state;
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "hovering"
|
||||
when: mouseArea.containsMouse
|
||||
StateChangeScript {
|
||||
script: {
|
||||
if (mouseArea.lastState == "") {
|
||||
Tablet.playSound(TabletEnums.ButtonHover);
|
||||
}
|
||||
mouseArea.lastState = mouseArea.state;
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
import QtQuick 2.6
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import Hifi.AvatarPackager.AvatarProjectStatus 1.0
|
||||
|
||||
import "../../controlsUit" 1.0 as HifiControls
|
||||
import "../../stylesUit" 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
property int colorScheme
|
||||
|
||||
property var footer: Item {
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 17
|
||||
HifiControls.Button {
|
||||
id: createButton
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
height: 30
|
||||
width: 133
|
||||
text: qsTr("Create")
|
||||
enabled: false
|
||||
onClicked: {
|
||||
let status = AvatarPackagerCore.createAvatarProject(projectLocation.text, name.text, avatarModel.text, textureFolder.text);
|
||||
if (status !== AvatarProjectStatus.SUCCESS) {
|
||||
avatarPackager.displayErrorMessage(status);
|
||||
return;
|
||||
}
|
||||
avatarProject.reset();
|
||||
avatarPackager.state = AvatarPackagerState.project;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visible: false
|
||||
anchors.fill: parent
|
||||
height: parent.height
|
||||
width: parent.width
|
||||
|
||||
function clearInputs() {
|
||||
name.text = projectLocation.text = avatarModel.text = textureFolder.text = "";
|
||||
}
|
||||
|
||||
function checkErrors() {
|
||||
let newErrorMessageText = "";
|
||||
|
||||
let projectName = name.text;
|
||||
let projectFolder = projectLocation.text;
|
||||
|
||||
let hasProjectNameError = projectName !== "" && projectFolder !== "" && !AvatarPackagerCore.isValidNewProjectName(projectFolder, projectName);
|
||||
|
||||
if (hasProjectNameError) {
|
||||
newErrorMessageText = "A folder with that name already exists at that location. Please choose a different project name or location.";
|
||||
}
|
||||
|
||||
name.error = projectLocation.error = hasProjectNameError;
|
||||
errorMessage.text = newErrorMessageText;
|
||||
createButton.enabled = newErrorMessageText === "" && requiredFieldsFilledIn();
|
||||
}
|
||||
|
||||
function requiredFieldsFilledIn() {
|
||||
return name.text !== "" && projectLocation.text !== "" && avatarModel.text !== "";
|
||||
}
|
||||
|
||||
RalewayRegular {
|
||||
id: errorMessage
|
||||
visible: text !== ""
|
||||
text: ""
|
||||
color: "#EA4C5F"
|
||||
wrapMode: Text.WordWrap
|
||||
size: 20
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: createAvatarColumns
|
||||
anchors.top: errorMessage.visible ? errorMessage.bottom : parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 10
|
||||
|
||||
spacing: 17
|
||||
|
||||
property string defaultFileBrowserPath: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH)
|
||||
|
||||
ProjectInputControl {
|
||||
id: name
|
||||
label: "Name"
|
||||
colorScheme: root.colorScheme
|
||||
onTextChanged: checkErrors()
|
||||
}
|
||||
|
||||
ProjectInputControl {
|
||||
id: projectLocation
|
||||
label: "Specify Project Location"
|
||||
colorScheme: root.colorScheme
|
||||
browseEnabled: true
|
||||
browseFolder: true
|
||||
browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath
|
||||
browseTitle: "Project Location"
|
||||
onTextChanged: checkErrors()
|
||||
}
|
||||
|
||||
ProjectInputControl {
|
||||
id: avatarModel
|
||||
label: "Specify Avatar Model (.fbx)"
|
||||
colorScheme: root.colorScheme
|
||||
browseEnabled: true
|
||||
browseFolder: false
|
||||
browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath
|
||||
browseFilter: "Avatar Model File (*.fbx)"
|
||||
browseTitle: "Open Avatar Model (.fbx)"
|
||||
onTextChanged: checkErrors()
|
||||
}
|
||||
|
||||
ProjectInputControl {
|
||||
id: textureFolder
|
||||
label: "Specify Texture Folder - <i>Optional</i>"
|
||||
colorScheme: root.colorScheme
|
||||
browseEnabled: true
|
||||
browseFolder: true
|
||||
browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath
|
||||
browseTitle: "Texture Folder"
|
||||
onTextChanged: checkErrors()
|
||||
}
|
||||
}
|
||||
}
|
120
interface/resources/qml/hifi/avatarPackager/InfoBox.qml
Normal file
120
interface/resources/qml/hifi/avatarPackager/InfoBox.qml
Normal file
|
@ -0,0 +1,120 @@
|
|||
import Hifi 1.0 as Hifi
|
||||
import QtQuick 2.5
|
||||
import stylesUit 1.0
|
||||
import controlsUit 1.0 as HifiControlsUit
|
||||
import "../../controls" as HifiControls
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
visible: false
|
||||
color: Qt.rgba(.34, .34, .34, 0.6)
|
||||
z: 999;
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
property alias title: titleText.text
|
||||
property alias content: loader.sourceComponent
|
||||
|
||||
property bool closeOnClickOutside: false;
|
||||
|
||||
property alias boxWidth: mainContainer.width
|
||||
property alias boxHeight: mainContainer.height
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
focus = true;
|
||||
}
|
||||
}
|
||||
|
||||
function open() {
|
||||
visible = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
HifiConstants {
|
||||
id: hifi
|
||||
}
|
||||
|
||||
// This object is always used in a popup.
|
||||
// This MouseArea is used to prevent a user from being
|
||||
// able to click on a button/mouseArea underneath the popup.
|
||||
MouseArea {
|
||||
anchors.fill: parent;
|
||||
propagateComposedEvents: false;
|
||||
hoverEnabled: true;
|
||||
onClicked: {
|
||||
if (closeOnClickOutside) {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: mainContainer
|
||||
|
||||
width: Math.max(parent.width * 0.8, 400)
|
||||
height: parent.height * 0.6
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
propagateComposedEvents: false
|
||||
hoverEnabled: true
|
||||
onClicked: function(ev) {
|
||||
ev.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
color: "#252525"
|
||||
|
||||
// TextStyle1
|
||||
RalewaySemiBold {
|
||||
id: titleText
|
||||
size: 24
|
||||
color: "white"
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 30
|
||||
|
||||
text: "Title not defined"
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.topMargin: 10
|
||||
anchors.top: titleText.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: button.top
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: button
|
||||
|
||||
height: 40
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 12
|
||||
|
||||
HifiControlsUit.Button {
|
||||
anchors.centerIn: parent
|
||||
|
||||
text: "CLOSE"
|
||||
onClicked: close()
|
||||
|
||||
color: hifi.buttons.noneBorderlessWhite;
|
||||
colorScheme: hifi.colorSchemes.dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import QtQuick 2.6
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtGraphicalEffects 1.0
|
||||
|
||||
AnimatedImage {
|
||||
id: root
|
||||
|
||||
width: 128
|
||||
height: 128
|
||||
|
||||
property bool white: false
|
||||
|
||||
source: white ? "../../../icons/loader-snake-256-wf.gif" : "../../../icons/loader-snake-256.gif"
|
||||
playing: true
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import QtQuick 2.6
|
||||
|
||||
import "../../controlsUit" 1.0 as HifiControls
|
||||
import "../../stylesUit" 1.0
|
||||
|
||||
Column {
|
||||
id: control
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 21
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 16
|
||||
|
||||
height: 75
|
||||
|
||||
spacing: 4
|
||||
|
||||
property alias label: label.text
|
||||
property alias browseEnabled: browseButton.visible
|
||||
property bool browseFolder: false
|
||||
property string browseFilter: "All Files (*.*)"
|
||||
property string browseTitle: "Open file"
|
||||
property string browseDir: ""
|
||||
property alias text: input.text
|
||||
property alias error: input.error
|
||||
|
||||
property int colorScheme
|
||||
|
||||
Row {
|
||||
RalewaySemiBold {
|
||||
id: label
|
||||
size: 20
|
||||
font.weight: Font.Medium
|
||||
text: ""
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
Row {
|
||||
width: control.width
|
||||
spacing: 16
|
||||
height: 40
|
||||
HifiControls.TextField {
|
||||
id: input
|
||||
colorScheme: control.colorScheme
|
||||
font.family: "Fira Sans"
|
||||
font.pixelSize: 18
|
||||
height: parent.height
|
||||
width: browseButton.visible ? parent.width - browseButton.width - parent.spacing : parent.width
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
id: browseButton
|
||||
visible: false
|
||||
height: parent.height
|
||||
width: 133
|
||||
text: qsTr("Browse")
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
avatarPackager.showModalOverlay = true;
|
||||
let browser = avatarPackager.desktopObject.fileDialog({
|
||||
selectDirectory: browseFolder,
|
||||
dir: browseDir,
|
||||
filter: browseFilter,
|
||||
title: browseTitle,
|
||||
});
|
||||
|
||||
browser.canceled.connect(function() {
|
||||
avatarPackager.showModalOverlay = false;
|
||||
});
|
||||
|
||||
browser.selectedFile.connect(function(fileUrl) {
|
||||
input.text = fileDialogHelper.urlToPath(fileUrl);
|
||||
avatarPackager.showModalOverlay = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import QtQuick 2.6
|
||||
|
||||
import "../../controlsUit" 1.0 as HifiControls
|
||||
import "../../stylesUit" 1.0
|
||||
|
||||
import TabletScriptingInterface 1.0
|
||||
|
||||
RalewaySemiBold {
|
||||
id: root
|
||||
|
||||
property color idleColor: "white"
|
||||
property color hoverColor: "#AFAFAF"
|
||||
property color pressedColor: "#575757"
|
||||
|
||||
color: clickable.hovered ? root.hoverColor : (clickable.pressed ? root.pressedColor : root.idleColor)
|
||||
|
||||
signal clicked()
|
||||
|
||||
ClickableArea {
|
||||
id: clickable
|
||||
|
||||
anchors.fill: root
|
||||
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
}
|
20
interface/resources/qml/hifi/avatarPackager/Style.qml
Normal file
20
interface/resources/qml/hifi/avatarPackager/Style.qml
Normal file
|
@ -0,0 +1,20 @@
|
|||
import QtQuick 2.5
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import "../../stylesUit" 1.0
|
||||
|
||||
QtObject {
|
||||
readonly property QtObject colors: QtObject {
|
||||
readonly property color lightGrayBackground: "#f2f2f2"
|
||||
readonly property color black: "#000000"
|
||||
readonly property color white: "#ffffff"
|
||||
readonly property color blueHighlight: "#00b4ef"
|
||||
readonly property color inputFieldBackground: "#d4d4d4"
|
||||
readonly property color yellowishOrange: "#ffb017"
|
||||
readonly property color blueAccent: "#0093c5"
|
||||
readonly property color greenHighlight: "#1fc6a6"
|
||||
readonly property color lightGray: "#afafaf"
|
||||
readonly property color redHighlight: "#ea4c5f"
|
||||
readonly property color orangeAccent: "#ff6309"
|
||||
}
|
||||
}
|
2
interface/resources/qml/hifi/avatarPackager/qmldir
Normal file
2
interface/resources/qml/hifi/avatarPackager/qmldir
Normal file
|
@ -0,0 +1,2 @@
|
|||
module AvatarPackager
|
||||
singleton AvatarPackagerState 1.0 AvatarPackagerState.qml
|
|
@ -157,7 +157,7 @@ Rectangle {
|
|||
visible = false;
|
||||
adjustWearablesClosed(status, avatarName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
|
@ -230,7 +230,7 @@ Rectangle {
|
|||
lineHeightMode: Text.FixedHeight
|
||||
lineHeight: 18;
|
||||
text: "Wearable"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
spacing: 10
|
||||
|
@ -241,7 +241,7 @@ Rectangle {
|
|||
lineHeight: 18;
|
||||
text: "<a href='#'>Get more</a>"
|
||||
linkColor: hifi.colors.blueHighlight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onLinkActivated: {
|
||||
popup.showGetWearables(function() {
|
||||
emitSendToScript({'method' : 'navigate', 'url' : 'hifi://AvatarIsland/11.5848,-8.10862,-2.80195'})
|
||||
|
|
|
@ -23,6 +23,8 @@ Rectangle {
|
|||
property string button2color: hifi.buttons.blue;
|
||||
property string button2text: ''
|
||||
|
||||
property bool closeOnClickOutside: false;
|
||||
|
||||
property var onButton2Clicked;
|
||||
property var onButton1Clicked;
|
||||
property var onLinkClicked;
|
||||
|
@ -56,6 +58,11 @@ Rectangle {
|
|||
anchors.fill: parent;
|
||||
propagateComposedEvents: false;
|
||||
hoverEnabled: true;
|
||||
onClicked: {
|
||||
if (closeOnClickOutside) {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
@ -68,6 +75,15 @@ Rectangle {
|
|||
console.debug('mainContainer: height = ', height)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent;
|
||||
propagateComposedEvents: false;
|
||||
hoverEnabled: true;
|
||||
onClicked: function(ev) {
|
||||
ev.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
color: "white"
|
||||
|
|
|
@ -35,7 +35,8 @@ Rectangle {
|
|||
|
||||
property real scaleValue: scaleSlider.value / 10
|
||||
property alias dominantHandIsLeft: leftHandRadioButton.checked
|
||||
property alias avatarCollisionsOn: collisionsEnabledRadiobutton.checked
|
||||
property alias otherAvatarsCollisionsOn: otherAvatarsCollisionsEnabledCheckBox.checked
|
||||
property alias environmentCollisionsOn: environmentCollisionsEnabledCheckBox.checked
|
||||
property alias avatarAnimationOverrideJSON: avatarAnimationUrlInputText.text
|
||||
property alias avatarAnimationJSON: avatarAnimationUrlInputText.placeholderText
|
||||
property alias avatarCollisionSoundUrl: avatarCollisionSoundUrlInputText.text
|
||||
|
@ -54,11 +55,11 @@ Rectangle {
|
|||
} else {
|
||||
rightHandRadioButton.checked = true;
|
||||
}
|
||||
|
||||
if (settings.otherAvatarsCollisionsEnabled) {
|
||||
otherAvatarsCollisionsEnabledCheckBox.checked = true;
|
||||
}
|
||||
if (settings.collisionsEnabled) {
|
||||
collisionsEnabledRadiobutton.checked = true;
|
||||
} else {
|
||||
collisionsDisabledRadioButton.checked = true;
|
||||
environmentCollisionsEnabledCheckBox.checked = true;
|
||||
}
|
||||
|
||||
avatarAnimationJSON = settings.animGraphUrl;
|
||||
|
@ -103,11 +104,11 @@ Rectangle {
|
|||
size: 17;
|
||||
text: "Avatar Scale"
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
@ -117,7 +118,7 @@ Rectangle {
|
|||
text: 'T'
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
HifiControlsUit.Slider {
|
||||
|
@ -135,7 +136,7 @@ Rectangle {
|
|||
}
|
||||
}
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
// TextStyle9
|
||||
|
@ -164,7 +165,7 @@ Rectangle {
|
|||
text: 'T'
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,55 +256,43 @@ Rectangle {
|
|||
text: "Right"
|
||||
boxSize: 20
|
||||
}
|
||||
|
||||
HifiConstants {
|
||||
id: hifi
|
||||
}
|
||||
|
||||
// TextStyle9
|
||||
RalewaySemiBold {
|
||||
size: 17;
|
||||
Layout.row: 1
|
||||
Layout.column: 0
|
||||
|
||||
text: "Avatar Collisions"
|
||||
text: "Avatar collides with other avatars"
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
id: onOff
|
||||
}
|
||||
|
||||
HifiControlsUit.RadioButton {
|
||||
id: collisionsEnabledRadiobutton
|
||||
|
||||
Layout.row: 1
|
||||
Layout.column: 1
|
||||
Layout.leftMargin: -40
|
||||
ButtonGroup.group: onOff
|
||||
|
||||
colorScheme: hifi.colorSchemes.light
|
||||
fontSize: 17
|
||||
letterSpacing: 1.4
|
||||
checked: true
|
||||
|
||||
text: "ON"
|
||||
boxSize: 20
|
||||
}
|
||||
|
||||
HifiConstants {
|
||||
id: hifi
|
||||
}
|
||||
|
||||
HifiControlsUit.RadioButton {
|
||||
id: collisionsDisabledRadioButton
|
||||
|
||||
HifiControlsUit.CheckBox {
|
||||
id: otherAvatarsCollisionsEnabledCheckBox;
|
||||
boxSize: 20;
|
||||
Layout.row: 1
|
||||
Layout.column: 2
|
||||
Layout.rightMargin: 20
|
||||
|
||||
ButtonGroup.group: onOff
|
||||
Layout.leftMargin: 60
|
||||
colorScheme: hifi.colorSchemes.light
|
||||
fontSize: 17
|
||||
letterSpacing: 1.4
|
||||
}
|
||||
|
||||
text: "OFF"
|
||||
boxSize: 20
|
||||
// TextStyle9
|
||||
RalewaySemiBold {
|
||||
size: 17;
|
||||
Layout.row: 2
|
||||
Layout.column: 0
|
||||
text: "Avatar collides with environment"
|
||||
}
|
||||
|
||||
HifiControlsUit.CheckBox {
|
||||
id: environmentCollisionsEnabledCheckBox;
|
||||
boxSize: 20;
|
||||
Layout.row: 2
|
||||
Layout.column: 2
|
||||
Layout.leftMargin: 60
|
||||
colorScheme: hifi.colorSchemes.light
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -327,8 +316,7 @@ Rectangle {
|
|||
InputTextStyle4 {
|
||||
id: avatarAnimationUrlInputText
|
||||
font.pixelSize: 17
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
Layout.fillWidth: true
|
||||
placeholderText: 'user\\file\\dir'
|
||||
|
||||
onFocusChanged: {
|
||||
|
@ -357,8 +345,7 @@ Rectangle {
|
|||
InputTextStyle4 {
|
||||
id: avatarCollisionSoundUrlInputText
|
||||
font.pixelSize: 17
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
Layout.fillWidth: true
|
||||
placeholderText: 'https://hifi-public.s3.amazonaws.com/sounds/Collisions-'
|
||||
|
||||
onFocusChanged: {
|
||||
|
|
15
interface/resources/qml/hifi/tablet/AvatarPackager.qml
Normal file
15
interface/resources/qml/hifi/tablet/AvatarPackager.qml
Normal file
|
@ -0,0 +1,15 @@
|
|||
import QtQuick 2.0
|
||||
import "../avatarPackager" 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
width: 480
|
||||
height: 706
|
||||
|
||||
AvatarPackagerApp {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
desktopObject: tabletRoot
|
||||
}
|
||||
}
|
|
@ -869,7 +869,7 @@ Flickable {
|
|||
id: outOfRangeDataStrategyComboBox
|
||||
|
||||
height: 25
|
||||
width: 100
|
||||
width: 150
|
||||
|
||||
editable: true
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
|
|
|
@ -158,6 +158,7 @@
|
|||
#include "audio/AudioScope.h"
|
||||
#include "avatar/AvatarManager.h"
|
||||
#include "avatar/MyHead.h"
|
||||
#include "avatar/AvatarPackager.h"
|
||||
#include "CrashRecoveryHandler.h"
|
||||
#include "CrashHandler.h"
|
||||
#include "devices/DdeFaceTracker.h"
|
||||
|
@ -170,6 +171,7 @@
|
|||
#include "scripting/Audio.h"
|
||||
#include "networking/CloseEventSender.h"
|
||||
#include "scripting/TestScriptingInterface.h"
|
||||
#include "scripting/PlatformInfoScriptingInterface.h"
|
||||
#include "scripting/AssetMappingsScriptingInterface.h"
|
||||
#include "scripting/ClipboardScriptingInterface.h"
|
||||
#include "scripting/DesktopScriptingInterface.h"
|
||||
|
@ -722,6 +724,8 @@ const QString TEST_RESULTS_LOCATION_COMMAND{ "--testResultsLocation" };
|
|||
bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
|
||||
const char** constArgv = const_cast<const char**>(argv);
|
||||
|
||||
qInstallMessageHandler(messageHandler);
|
||||
|
||||
// HRS: I could not figure out how to move these any earlier in startup, so when using this option, be sure to also supply
|
||||
// --allowMultipleInstances
|
||||
auto reportAndQuit = [&](const char* commandSwitch, std::function<void(FILE* fp)> report) {
|
||||
|
@ -861,7 +865,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
|
|||
DependencyManager::set<LODManager>();
|
||||
DependencyManager::set<StandAloneJSConsole>();
|
||||
DependencyManager::set<DialogsManager>();
|
||||
DependencyManager::set<BandwidthRecorder>();
|
||||
DependencyManager::set<ResourceCacheSharedItems>();
|
||||
DependencyManager::set<DesktopScriptingInterface>();
|
||||
DependencyManager::set<EntityScriptingInterface>(true);
|
||||
|
@ -922,6 +925,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
|
|||
DependencyManager::set<Keyboard>();
|
||||
DependencyManager::set<KeyboardScriptingInterface>();
|
||||
DependencyManager::set<GrabManager>();
|
||||
DependencyManager::set<AvatarPackager>();
|
||||
|
||||
return previousSessionCrashed;
|
||||
}
|
||||
|
@ -972,6 +976,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
QApplication(argc, argv),
|
||||
_window(new MainWindow(desktop())),
|
||||
_sessionRunTimer(startupTimer),
|
||||
_logger(new FileLogger(this)),
|
||||
_previousSessionCrashed(setupEssentials(argc, argv, runningMarkerExisted)),
|
||||
_entitySimulation(new PhysicalEntitySimulation()),
|
||||
_physicsEngine(new PhysicsEngine(Vectors::ZERO)),
|
||||
|
@ -1061,9 +1066,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
installNativeEventFilter(&MyNativeEventFilter::getInstance());
|
||||
#endif
|
||||
|
||||
_logger = new FileLogger(this);
|
||||
qInstallMessageHandler(messageHandler);
|
||||
|
||||
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf");
|
||||
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/fontawesome-webfont.ttf");
|
||||
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/hifi-glyphs.ttf");
|
||||
|
@ -1577,13 +1579,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
|
||||
connect(this, SIGNAL(aboutToQuit()), this, SLOT(onAboutToQuit()));
|
||||
|
||||
// hook up bandwidth estimator
|
||||
QSharedPointer<BandwidthRecorder> bandwidthRecorder = DependencyManager::get<BandwidthRecorder>();
|
||||
connect(nodeList.data(), &LimitedNodeList::dataSent,
|
||||
bandwidthRecorder.data(), &BandwidthRecorder::updateOutboundData);
|
||||
connect(nodeList.data(), &LimitedNodeList::dataReceived,
|
||||
bandwidthRecorder.data(), &BandwidthRecorder::updateInboundData);
|
||||
|
||||
// FIXME -- I'm a little concerned about this.
|
||||
connect(myAvatar->getSkeletonModel().get(), &SkeletonModel::skeletonLoaded,
|
||||
this, &Application::checkSkeleton, Qt::QueuedConnection);
|
||||
|
@ -2049,15 +2044,12 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
properties["deadlock_watchdog_maxElapsed"] = (int)DeadlockWatchdogThread::_maxElapsed;
|
||||
properties["deadlock_watchdog_maxElapsedAverage"] = (int)DeadlockWatchdogThread::_maxElapsedAverage;
|
||||
|
||||
auto bandwidthRecorder = DependencyManager::get<BandwidthRecorder>();
|
||||
properties["packet_rate_in"] = bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond();
|
||||
properties["packet_rate_out"] = bandwidthRecorder->getCachedTotalAverageOutputPacketsPerSecond();
|
||||
properties["kbps_in"] = bandwidthRecorder->getCachedTotalAverageInputKilobitsPerSecond();
|
||||
properties["kbps_out"] = bandwidthRecorder->getCachedTotalAverageOutputKilobitsPerSecond();
|
||||
|
||||
properties["atp_in_kbps"] = bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AssetServer);
|
||||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
properties["packet_rate_in"] = nodeList->getInboundPPS();
|
||||
properties["packet_rate_out"] = nodeList->getOutboundPPS();
|
||||
properties["kbps_in"] = nodeList->getInboundKbps();
|
||||
properties["kbps_out"] = nodeList->getOutboundKbps();
|
||||
|
||||
SharedNodePointer entityServerNode = nodeList->soloNodeOfType(NodeType::EntityServer);
|
||||
SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer);
|
||||
SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer);
|
||||
|
@ -2068,6 +2060,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
properties["avatar_ping"] = avatarMixerNode ? avatarMixerNode->getPingMs() : -1;
|
||||
properties["asset_ping"] = assetServerNode ? assetServerNode->getPingMs() : -1;
|
||||
properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1;
|
||||
properties["atp_in_kbps"] = assetServerNode ? assetServerNode->getInboundKbps() : 0.0f;
|
||||
|
||||
auto loadingRequests = ResourceCache::getLoadingRequests();
|
||||
|
||||
|
@ -2293,7 +2286,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
|
||||
// Setup the mouse ray pick and related operators
|
||||
{
|
||||
auto mouseRayPick = std::make_shared<RayPick>(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_INCLUDE_NONCOLLIDABLE()), 0.0f, true);
|
||||
auto mouseRayPick = std::make_shared<RayPick>(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES()), 0.0f, true);
|
||||
mouseRayPick->parentTransform = std::make_shared<MouseTransformNode>();
|
||||
mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE);
|
||||
auto mouseRayPickID = DependencyManager::get<PickManager>()->addPick(PickQuery::Ray, mouseRayPick);
|
||||
|
@ -2466,6 +2459,9 @@ void Application::updateHeartbeat() const {
|
|||
}
|
||||
|
||||
void Application::onAboutToQuit() {
|
||||
// quickly save AvatarEntityData before the EntityTree is dismantled
|
||||
getMyAvatar()->saveAvatarEntityDataToSettings();
|
||||
|
||||
emit beforeAboutToQuit();
|
||||
|
||||
if (getLoginDialogPoppedUp() && _firstRun.get()) {
|
||||
|
@ -2626,6 +2622,7 @@ void Application::cleanupBeforeQuit() {
|
|||
DependencyManager::destroy<PickManager>();
|
||||
DependencyManager::destroy<KeyboardScriptingInterface>();
|
||||
DependencyManager::destroy<Keyboard>();
|
||||
DependencyManager::destroy<AvatarPackager>();
|
||||
|
||||
qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete";
|
||||
}
|
||||
|
@ -5970,6 +5967,8 @@ void Application::update(float deltaTime) {
|
|||
if (deltaTime > FLT_EPSILON) {
|
||||
myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH));
|
||||
myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW));
|
||||
myAvatar->setDriveKey(MyAvatar::DELTA_PITCH, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_PITCH));
|
||||
myAvatar->setDriveKey(MyAvatar::DELTA_YAW, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_YAW));
|
||||
myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW));
|
||||
}
|
||||
}
|
||||
|
@ -6759,8 +6758,10 @@ void Application::updateWindowTitle() const {
|
|||
}
|
||||
|
||||
void Application::clearDomainOctreeDetails() {
|
||||
// before we delete all entities get MyAvatar's AvatarEntityData ready
|
||||
getMyAvatar()->prepareAvatarEntityDataForReload();
|
||||
|
||||
// if we're about to quit, we really don't need to do any of these things...
|
||||
// if we're about to quit, we really don't need to do the rest of these things...
|
||||
if (_aboutToQuit) {
|
||||
return;
|
||||
}
|
||||
|
@ -6788,8 +6789,6 @@ void Application::clearDomainOctreeDetails() {
|
|||
ShaderCache::instance().clearUnusedResources();
|
||||
DependencyManager::get<TextureCache>()->clearUnusedResources();
|
||||
DependencyManager::get<recording::ClipCache>()->clearUnusedResources();
|
||||
|
||||
getMyAvatar()->setAvatarEntityDataChanged(true);
|
||||
}
|
||||
|
||||
void Application::domainURLChanged(QUrl domainURL) {
|
||||
|
@ -6998,6 +6997,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe
|
|||
scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance());
|
||||
}
|
||||
|
||||
scriptEngine->registerGlobalObject("PlatformInfo", PlatformInfoScriptingInterface::getInstance());
|
||||
scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this));
|
||||
|
||||
// hook our avatar and avatar hash map object into this script engine
|
||||
|
@ -8715,6 +8715,14 @@ void Application::updateLoginDialogOverlayPosition() {
|
|||
}
|
||||
}
|
||||
|
||||
bool Application::hasRiftControllers() {
|
||||
return PluginUtils::isOculusTouchControllerAvailable();
|
||||
}
|
||||
|
||||
bool Application::hasViveControllers() {
|
||||
return PluginUtils::isViveControllerAvailable();
|
||||
}
|
||||
|
||||
void Application::onDismissedLoginDialog() {
|
||||
_loginDialogPoppedUp = false;
|
||||
loginDialogPoppedUp.set(false);
|
||||
|
@ -8933,6 +8941,10 @@ void Application::copyToClipboard(const QString& text) {
|
|||
QApplication::clipboard()->setText(text);
|
||||
}
|
||||
|
||||
QString Application::getGraphicsCardType() {
|
||||
return GPUIdent::getInstance()->getName();
|
||||
}
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
void Application::beforeEnterBackground() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
|
|
@ -326,6 +326,10 @@ public:
|
|||
void createLoginDialogOverlay();
|
||||
void updateLoginDialogOverlayPosition();
|
||||
|
||||
// Check if a headset is connected
|
||||
bool hasRiftControllers();
|
||||
bool hasViveControllers();
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
void beforeEnterBackground();
|
||||
void enterBackground();
|
||||
|
@ -459,6 +463,8 @@ public slots:
|
|||
|
||||
void changeViewAsNeeded(float boomLength);
|
||||
|
||||
QString getGraphicsCardType();
|
||||
|
||||
private slots:
|
||||
void onDesktopRootItemCreated(QQuickItem* qmlContext);
|
||||
void onDesktopRootContextCreated(QQmlContext* qmlContext);
|
||||
|
@ -588,6 +594,8 @@ private:
|
|||
|
||||
bool _aboutToQuit { false };
|
||||
|
||||
FileLogger* _logger { nullptr };
|
||||
|
||||
bool _previousSessionCrashed;
|
||||
|
||||
DisplayPluginPointer _displayPlugin;
|
||||
|
@ -668,8 +676,6 @@ private:
|
|||
QPointer<EntityScriptServerLogDialog> _entityScriptServerLogDialog;
|
||||
QDir _defaultScriptsLocation;
|
||||
|
||||
FileLogger* _logger;
|
||||
|
||||
TouchEvent _lastTouchEvent;
|
||||
|
||||
quint64 _lastNackTime;
|
||||
|
@ -787,6 +793,5 @@ private:
|
|||
|
||||
bool _showTrackedObjects { false };
|
||||
bool _prevShowTrackedObjects { false };
|
||||
|
||||
};
|
||||
#endif // hifi_Application_h
|
||||
|
|
|
@ -60,7 +60,6 @@ void addAvatarEntities(const QVariantList& avatarEntities) {
|
|||
entityProperties.setParentID(myNodeID);
|
||||
entityProperties.setEntityHostType(entity::HostType::AVATAR);
|
||||
entityProperties.setOwningAvatarID(myNodeID);
|
||||
entityProperties.setSimulationOwner(myNodeID, AVATAR_ENTITY_SIMULATION_PRIORITY);
|
||||
entityProperties.markAllChanged();
|
||||
|
||||
EntityItemID id = EntityItemID(QUuid::createUuid());
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
#include "assets/ATPAssetMigrator.h"
|
||||
#include "audio/AudioScope.h"
|
||||
#include "avatar/AvatarManager.h"
|
||||
#include "avatar/AvatarPackager.h"
|
||||
#include "AvatarBookmarks.h"
|
||||
#include "devices/DdeFaceTracker.h"
|
||||
#include "MainWindow.h"
|
||||
|
@ -144,9 +145,13 @@ Menu::Menu() {
|
|||
assetServerAction->setEnabled(nodeList->getThisNodeCanWriteAssets());
|
||||
}
|
||||
|
||||
// Edit > Package Avatar as .fst...
|
||||
addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0,
|
||||
qApp, SLOT(packageModel()));
|
||||
// Edit > Avatar Packager
|
||||
#ifndef Q_OS_ANDROID
|
||||
action = addActionToQMenuAndActionHash(editMenu, MenuOption::AvatarPackager);
|
||||
connect(action, &QAction::triggered, [] {
|
||||
DependencyManager::get<AvatarPackager>()->open();
|
||||
});
|
||||
#endif
|
||||
|
||||
// Edit > Reload All Content
|
||||
addActionToQMenuAndActionHash(editMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches()));
|
||||
|
@ -645,6 +650,8 @@ Menu::Menu() {
|
|||
|
||||
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowTrackedObjects, 0, false, qApp, SLOT(setShowTrackedObjects(bool)));
|
||||
|
||||
addActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::PackageModel, 0, qApp, SLOT(packageModel()));
|
||||
|
||||
// Developer > Hands >>>
|
||||
MenuWrapper* handOptionsMenu = developerMenu->addMenu("Hands");
|
||||
addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false,
|
||||
|
|
|
@ -46,6 +46,7 @@ namespace MenuOption {
|
|||
const QString AutoMuteAudio = "Auto Mute Microphone";
|
||||
const QString AvatarReceiveStats = "Show Receive Stats";
|
||||
const QString AvatarBookmarks = "Avatar Bookmarks";
|
||||
const QString AvatarPackager = "Avatar Packager";
|
||||
const QString Back = "Back";
|
||||
const QString BinaryEyelidControl = "Binary Eyelid Control";
|
||||
const QString BookmarkAvatar = "Bookmark Avatar";
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
|
||||
#include <shared/QtHelpers.h>
|
||||
#include <AvatarData.h>
|
||||
#include <PerfStat.h>
|
||||
|
@ -268,6 +267,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
|
|||
if (avatar->getSkeletonModel()->isLoaded()) {
|
||||
// remove the orb if it is there
|
||||
avatar->removeOrb();
|
||||
avatar->updateCollisionGroup(_myAvatar->getOtherAvatarsCollisionsEnabled());
|
||||
if (avatar->needsPhysicsUpdate()) {
|
||||
_avatarsToChangeInPhysics.insert(avatar);
|
||||
}
|
||||
|
@ -529,6 +529,7 @@ void AvatarManager::handleChangedMotionStates(const VectorOfMotionStates& motion
|
|||
}
|
||||
|
||||
void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents) {
|
||||
bool playedCollisionSound { false };
|
||||
for (Collision collision : collisionEvents) {
|
||||
// TODO: The plan is to handle MOTIONSTATE_TYPE_AVATAR, and then MOTIONSTATE_TYPE_MYAVATAR. As it is, other
|
||||
// people's avatars will have an id that doesn't match any entities, and one's own avatar will have
|
||||
|
@ -536,43 +537,47 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents
|
|||
// my avatar. (Other user machines will make a similar analysis and inject sound for their collisions.)
|
||||
if (collision.idA.isNull() || collision.idB.isNull()) {
|
||||
auto myAvatar = getMyAvatar();
|
||||
auto collisionSound = myAvatar->getCollisionSound();
|
||||
if (collisionSound) {
|
||||
const auto characterController = myAvatar->getCharacterController();
|
||||
const float avatarVelocityChange = (characterController ? glm::length(characterController->getVelocityChange()) : 0.0f);
|
||||
const float velocityChange = glm::length(collision.velocityChange) + avatarVelocityChange;
|
||||
const float MIN_AVATAR_COLLISION_ACCELERATION = 2.4f; // walking speed
|
||||
const bool isSound = (collision.type == CONTACT_EVENT_TYPE_START) && (velocityChange > MIN_AVATAR_COLLISION_ACCELERATION);
|
||||
myAvatar->collisionWithEntity(collision);
|
||||
|
||||
if (!isSound) {
|
||||
return; // No sense iterating for others. We only have one avatar.
|
||||
if (!playedCollisionSound) {
|
||||
playedCollisionSound = true;
|
||||
auto collisionSound = myAvatar->getCollisionSound();
|
||||
if (collisionSound) {
|
||||
const auto characterController = myAvatar->getCharacterController();
|
||||
const float avatarVelocityChange =
|
||||
(characterController ? glm::length(characterController->getVelocityChange()) : 0.0f);
|
||||
const float velocityChange = glm::length(collision.velocityChange) + avatarVelocityChange;
|
||||
const float MIN_AVATAR_COLLISION_ACCELERATION = 2.4f; // walking speed
|
||||
const bool isSound =
|
||||
(collision.type == CONTACT_EVENT_TYPE_START) && (velocityChange > MIN_AVATAR_COLLISION_ACCELERATION);
|
||||
|
||||
if (!isSound) {
|
||||
return; // No sense iterating for others. We only have one avatar.
|
||||
}
|
||||
// Your avatar sound is personal to you, so let's say the "mass" part of the kinetic energy is already accounted for.
|
||||
const float energy = velocityChange * velocityChange;
|
||||
const float COLLISION_ENERGY_AT_FULL_VOLUME = 10.0f;
|
||||
const float energyFactorOfFull = fmin(1.0f, energy / COLLISION_ENERGY_AT_FULL_VOLUME);
|
||||
|
||||
// For general entity collisionSoundURL, playSound supports changing the pitch for the sound based on the size of the object,
|
||||
// but most avatars are roughly the same size, so let's not be so fancy yet.
|
||||
const float AVATAR_STRETCH_FACTOR = 1.0f;
|
||||
|
||||
_collisionInjectors.remove_if(
|
||||
[](const AudioInjectorPointer& injector) { return !injector || injector->isFinished(); });
|
||||
|
||||
static const int MAX_INJECTOR_COUNT = 3;
|
||||
if (_collisionInjectors.size() < MAX_INJECTOR_COUNT) {
|
||||
AudioInjectorOptions options;
|
||||
options.stereo = collisionSound->isStereo();
|
||||
options.position = myAvatar->getWorldPosition();
|
||||
options.volume = energyFactorOfFull;
|
||||
options.pitch = 1.0f / AVATAR_STRETCH_FACTOR;
|
||||
|
||||
auto injector = AudioInjector::playSoundAndDelete(collisionSound, options);
|
||||
_collisionInjectors.emplace_back(injector);
|
||||
}
|
||||
}
|
||||
// Your avatar sound is personal to you, so let's say the "mass" part of the kinetic energy is already accounted for.
|
||||
const float energy = velocityChange * velocityChange;
|
||||
const float COLLISION_ENERGY_AT_FULL_VOLUME = 10.0f;
|
||||
const float energyFactorOfFull = fmin(1.0f, energy / COLLISION_ENERGY_AT_FULL_VOLUME);
|
||||
|
||||
// For general entity collisionSoundURL, playSound supports changing the pitch for the sound based on the size of the object,
|
||||
// but most avatars are roughly the same size, so let's not be so fancy yet.
|
||||
const float AVATAR_STRETCH_FACTOR = 1.0f;
|
||||
|
||||
_collisionInjectors.remove_if([](const AudioInjectorPointer& injector) {
|
||||
return !injector || injector->isFinished();
|
||||
});
|
||||
|
||||
static const int MAX_INJECTOR_COUNT = 3;
|
||||
if (_collisionInjectors.size() < MAX_INJECTOR_COUNT) {
|
||||
AudioInjectorOptions options;
|
||||
options.stereo = collisionSound->isStereo();
|
||||
options.position = myAvatar->getWorldPosition();
|
||||
options.volume = energyFactorOfFull;
|
||||
options.pitch = 1.0f / AVATAR_STRETCH_FACTOR;
|
||||
|
||||
auto injector = AudioInjector::playSoundAndDelete(collisionSound, options);
|
||||
_collisionInjectors.emplace_back(injector);
|
||||
}
|
||||
myAvatar->collisionWithEntity(collision);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
AvatarMotionState::AvatarMotionState(OtherAvatarPointer avatar, const btCollisionShape* shape) : ObjectMotionState(shape), _avatar(avatar) {
|
||||
assert(_avatar);
|
||||
_type = MOTIONSTATE_TYPE_AVATAR;
|
||||
_collisionGroup = BULLET_COLLISION_GROUP_OTHER_AVATAR;
|
||||
cacheShapeDiameter();
|
||||
}
|
||||
|
||||
|
@ -170,8 +171,8 @@ QUuid AvatarMotionState::getSimulatorID() const {
|
|||
|
||||
// virtual
|
||||
void AvatarMotionState::computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const {
|
||||
group = BULLET_COLLISION_GROUP_OTHER_AVATAR;
|
||||
mask = Physics::getDefaultCollisionMask(group);
|
||||
group = _collisionGroup;
|
||||
mask = _collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS ? 0 : Physics::getDefaultCollisionMask(group);
|
||||
}
|
||||
|
||||
// virtual
|
||||
|
|
|
@ -66,6 +66,9 @@ public:
|
|||
|
||||
void addDirtyFlags(uint32_t flags) { _dirtyFlags |= flags; }
|
||||
|
||||
void setCollisionGroup(int32_t group) { _collisionGroup = group; }
|
||||
int32_t getCollisionGroup() { return _collisionGroup; }
|
||||
|
||||
virtual void computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const override;
|
||||
|
||||
virtual float getMass() const override;
|
||||
|
@ -87,7 +90,7 @@ protected:
|
|||
|
||||
OtherAvatarPointer _avatar;
|
||||
float _diameter { 0.0f };
|
||||
|
||||
int32_t _collisionGroup;
|
||||
uint32_t _dirtyFlags;
|
||||
};
|
||||
|
||||
|
|
149
interface/src/avatar/AvatarPackager.cpp
Normal file
149
interface/src/avatar/AvatarPackager.cpp
Normal file
|
@ -0,0 +1,149 @@
|
|||
//
|
||||
// AvatarPackager.cpp
|
||||
//
|
||||
//
|
||||
// Created by Thijs Wenker on 12/6/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
|
||||
//
|
||||
|
||||
#include "AvatarPackager.h"
|
||||
|
||||
#include "Application.h"
|
||||
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
#include <QUrl>
|
||||
|
||||
#include <OffscreenUi.h>
|
||||
#include "ModelSelector.h"
|
||||
#include <avatar/MarketplaceItemUploader.h>
|
||||
|
||||
#include <mutex>
|
||||
#include "ui/TabletScriptingInterface.h"
|
||||
|
||||
std::once_flag setupQMLTypesFlag;
|
||||
AvatarPackager::AvatarPackager() {
|
||||
std::call_once(setupQMLTypesFlag, []() {
|
||||
qmlRegisterType<FST>();
|
||||
qmlRegisterType<MarketplaceItemUploader>();
|
||||
qRegisterMetaType<AvatarPackager*>();
|
||||
qRegisterMetaType<AvatarProject*>();
|
||||
qRegisterMetaType<AvatarProjectStatus::AvatarProjectStatus>();
|
||||
qmlRegisterUncreatableMetaObject(
|
||||
AvatarProjectStatus::staticMetaObject,
|
||||
"Hifi.AvatarPackager.AvatarProjectStatus",
|
||||
1, 0,
|
||||
"AvatarProjectStatus",
|
||||
"Error: only enums"
|
||||
);
|
||||
});
|
||||
|
||||
recentProjectsFromVariantList(_recentProjectsSetting.get());
|
||||
|
||||
QDir defaultProjectsDir(AvatarProject::getDefaultProjectsPath());
|
||||
defaultProjectsDir.mkpath(".");
|
||||
}
|
||||
|
||||
bool AvatarPackager::open() {
|
||||
const auto packageModelDialogCreated = [=](QQmlContext* context, QObject* newObject) {
|
||||
context->setContextProperty("AvatarPackagerCore", this);
|
||||
};
|
||||
|
||||
static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system";
|
||||
auto tablet = dynamic_cast<TabletProxy*>(DependencyManager::get<TabletScriptingInterface>()->getTablet(SYSTEM_TABLET));
|
||||
|
||||
if (tablet->getToolbarMode()) {
|
||||
static const QUrl url{ "hifi/AvatarPackagerWindow.qml" };
|
||||
DependencyManager::get<OffscreenUi>()->show(url, "AvatarPackager", packageModelDialogCreated);
|
||||
return true;
|
||||
}
|
||||
|
||||
static const QUrl url{ "hifi/tablet/AvatarPackager.qml" };
|
||||
if (!tablet->isPathLoaded(url)) {
|
||||
tablet->getTabletSurface()->getSurfaceContext()->setContextProperty("AvatarPackagerCore", this);
|
||||
tablet->pushOntoStack(url);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AvatarPackager::addCurrentProjectToRecentProjects() {
|
||||
const int MAX_RECENT_PROJECTS = 5;
|
||||
const QString& fstPath = _currentAvatarProject->getFSTPath();
|
||||
auto removeProjects = QVector<RecentAvatarProject>();
|
||||
for (const auto& project : _recentProjects) {
|
||||
if (project.getProjectFSTPath() == fstPath) {
|
||||
removeProjects.append(project);
|
||||
}
|
||||
}
|
||||
for (const auto& removeProject : removeProjects) {
|
||||
_recentProjects.removeOne(removeProject);
|
||||
}
|
||||
|
||||
const auto newRecentProject = RecentAvatarProject(_currentAvatarProject->getProjectName(), fstPath);
|
||||
_recentProjects.prepend(newRecentProject);
|
||||
|
||||
while (_recentProjects.size() > MAX_RECENT_PROJECTS) {
|
||||
_recentProjects.pop_back();
|
||||
}
|
||||
|
||||
_recentProjectsSetting.set(recentProjectsToVariantList(false));
|
||||
emit recentProjectsChanged();
|
||||
}
|
||||
|
||||
QVariantList AvatarPackager::recentProjectsToVariantList(bool includeProjectPaths) const {
|
||||
QVariantList result;
|
||||
for (const auto& project : _recentProjects) {
|
||||
QVariantMap projectVariant;
|
||||
projectVariant.insert("name", project.getProjectName());
|
||||
projectVariant.insert("path", project.getProjectFSTPath());
|
||||
if (includeProjectPaths) {
|
||||
projectVariant.insert("projectPath", project.getProjectPath());
|
||||
}
|
||||
result.append(projectVariant);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
void AvatarPackager::recentProjectsFromVariantList(QVariantList projectsVariant) {
|
||||
_recentProjects.clear();
|
||||
for (const auto& projectVariant : projectsVariant) {
|
||||
auto map = projectVariant.toMap();
|
||||
_recentProjects.append(RecentAvatarProject(map.value("name").toString(), map.value("path").toString()));
|
||||
}
|
||||
}
|
||||
|
||||
AvatarProjectStatus::AvatarProjectStatus AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) {
|
||||
AvatarProjectStatus::AvatarProjectStatus status;
|
||||
setAvatarProject(AvatarProject::openAvatarProject(avatarProjectFSTPath, status));
|
||||
return status;
|
||||
}
|
||||
|
||||
AvatarProjectStatus::AvatarProjectStatus AvatarPackager::createAvatarProject(const QString& projectsFolder,
|
||||
const QString& avatarProjectName,
|
||||
const QString& avatarModelPath,
|
||||
const QString& textureFolder) {
|
||||
AvatarProjectStatus::AvatarProjectStatus status;
|
||||
setAvatarProject(AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder, status));
|
||||
return status;
|
||||
}
|
||||
|
||||
void AvatarPackager::setAvatarProject(AvatarProject* avatarProject) {
|
||||
if (avatarProject == _currentAvatarProject) {
|
||||
return;
|
||||
}
|
||||
if (_currentAvatarProject) {
|
||||
_currentAvatarProject->deleteLater();
|
||||
}
|
||||
_currentAvatarProject = avatarProject;
|
||||
if (_currentAvatarProject) {
|
||||
addCurrentProjectToRecentProjects();
|
||||
connect(_currentAvatarProject, &AvatarProject::nameChanged, this, &AvatarPackager::addCurrentProjectToRecentProjects);
|
||||
QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership);
|
||||
}
|
||||
emit avatarProjectChanged();
|
||||
}
|
100
interface/src/avatar/AvatarPackager.h
Normal file
100
interface/src/avatar/AvatarPackager.h
Normal file
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// AvatarPackager.h
|
||||
//
|
||||
//
|
||||
// Created by Thijs Wenker on 12/6/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
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#ifndef hifi_AvatarPackager_h
|
||||
#define hifi_AvatarPackager_h
|
||||
|
||||
#include <QObject>
|
||||
#include <DependencyManager.h>
|
||||
|
||||
#include "FileDialogHelper.h"
|
||||
|
||||
#include "avatar/AvatarProject.h"
|
||||
#include "SettingHandle.h"
|
||||
|
||||
class RecentAvatarProject {
|
||||
public:
|
||||
RecentAvatarProject() = default;
|
||||
|
||||
|
||||
RecentAvatarProject(QString projectName, QString projectFSTPath) {
|
||||
_projectName = projectName;
|
||||
_projectFSTPath = projectFSTPath;
|
||||
}
|
||||
RecentAvatarProject(const RecentAvatarProject& other) {
|
||||
_projectName = other._projectName;
|
||||
_projectFSTPath = other._projectFSTPath;
|
||||
}
|
||||
|
||||
QString getProjectName() const { return _projectName; }
|
||||
|
||||
QString getProjectFSTPath() const { return _projectFSTPath; }
|
||||
|
||||
QString getProjectPath() const {
|
||||
return QFileInfo(_projectFSTPath).absoluteDir().absolutePath();
|
||||
}
|
||||
|
||||
bool operator==(const RecentAvatarProject& other) const {
|
||||
return _projectName == other._projectName && _projectFSTPath == other._projectFSTPath;
|
||||
}
|
||||
|
||||
private:
|
||||
QString _projectName;
|
||||
QString _projectFSTPath;
|
||||
|
||||
};
|
||||
|
||||
class AvatarPackager : public QObject, public Dependency {
|
||||
Q_OBJECT
|
||||
SINGLETON_DEPENDENCY
|
||||
Q_PROPERTY(AvatarProject* currentAvatarProject READ getAvatarProject NOTIFY avatarProjectChanged)
|
||||
Q_PROPERTY(QString AVATAR_PROJECTS_PATH READ getAvatarProjectsPath CONSTANT)
|
||||
Q_PROPERTY(QVariantList recentProjects READ getRecentProjects NOTIFY recentProjectsChanged)
|
||||
public:
|
||||
AvatarPackager();
|
||||
bool open();
|
||||
|
||||
Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus createAvatarProject(const QString& projectsFolder,
|
||||
const QString& avatarProjectName,
|
||||
const QString& avatarModelPath,
|
||||
const QString& textureFolder);
|
||||
|
||||
Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus openAvatarProject(const QString& avatarProjectFSTPath);
|
||||
Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) const {
|
||||
return AvatarProject::isValidNewProjectName(projectPath, projectName);
|
||||
}
|
||||
|
||||
signals:
|
||||
void avatarProjectChanged();
|
||||
void recentProjectsChanged();
|
||||
|
||||
private:
|
||||
Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; };
|
||||
Q_INVOKABLE QString getAvatarProjectsPath() const { return AvatarProject::getDefaultProjectsPath(); }
|
||||
Q_INVOKABLE QVariantList getRecentProjects() const { return recentProjectsToVariantList(true); }
|
||||
|
||||
void setAvatarProject(AvatarProject* avatarProject);
|
||||
|
||||
void addCurrentProjectToRecentProjects();
|
||||
|
||||
AvatarProject* _currentAvatarProject { nullptr };
|
||||
QVector<RecentAvatarProject> _recentProjects;
|
||||
|
||||
QVariantList recentProjectsToVariantList(bool includeProjectPaths) const;
|
||||
|
||||
void recentProjectsFromVariantList(QVariantList projectsVariant);
|
||||
|
||||
|
||||
Setting::Handle<QVariantList> _recentProjectsSetting { "io.highfidelity.avatarPackager.recentProjects", QVariantList() };
|
||||
};
|
||||
|
||||
#endif // hifi_AvatarPackager_h
|
260
interface/src/avatar/AvatarProject.cpp
Normal file
260
interface/src/avatar/AvatarProject.cpp
Normal file
|
@ -0,0 +1,260 @@
|
|||
//
|
||||
// AvatarProject.cpp
|
||||
//
|
||||
//
|
||||
// Created by Thijs Wenker on 12/7/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
|
||||
//
|
||||
|
||||
#include "AvatarProject.h"
|
||||
|
||||
#include <FSTReader.h>
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QQmlEngine>
|
||||
#include <QTimer>
|
||||
|
||||
#include "FBXSerializer.h"
|
||||
#include <ui/TabletScriptingInterface.h>
|
||||
#include "scripting/HMDScriptingInterface.h"
|
||||
|
||||
AvatarProject* AvatarProject::openAvatarProject(const QString& path, AvatarProjectStatus::AvatarProjectStatus& status) {
|
||||
status = AvatarProjectStatus::NONE;
|
||||
|
||||
if (!path.toLower().endsWith(".fst")) {
|
||||
status = AvatarProjectStatus::ERROR_OPEN_INVALID_FILE_TYPE;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QFileInfo fstFileInfo{ path };
|
||||
if (!fstFileInfo.absoluteDir().exists()) {
|
||||
status = AvatarProjectStatus::ERROR_OPEN_PROJECT_FOLDER;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!fstFileInfo.exists()) {
|
||||
status = AvatarProjectStatus::ERROR_OPEN_FIND_FST;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QFile file{ fstFileInfo.filePath() };
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
status = AvatarProjectStatus::ERROR_OPEN_OPEN_FST;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto project = new AvatarProject(path, file.readAll());
|
||||
|
||||
QFileInfo fbxFileInfo{ project->getFBXPath() };
|
||||
if (!fbxFileInfo.exists()) {
|
||||
project->deleteLater();
|
||||
status = AvatarProjectStatus::ERROR_OPEN_FIND_MODEL;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QQmlEngine::setObjectOwnership(project, QQmlEngine::CppOwnership);
|
||||
status = AvatarProjectStatus::SUCCESS;
|
||||
return project;
|
||||
}
|
||||
|
||||
AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName,
|
||||
const QString& avatarModelPath, const QString& textureFolder,
|
||||
AvatarProjectStatus::AvatarProjectStatus& status) {
|
||||
status = AvatarProjectStatus::NONE;
|
||||
|
||||
if (!isValidNewProjectName(projectsFolder, avatarProjectName)) {
|
||||
status = AvatarProjectStatus::ERROR_CREATE_PROJECT_NAME;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QDir projectDir(projectsFolder + "/" + avatarProjectName);
|
||||
if (!projectDir.mkpath(".")) {
|
||||
status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QDir projectTexturesDir(projectDir.path() + "/textures");
|
||||
if (!projectTexturesDir.mkpath(".")) {
|
||||
status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QDir projectScriptsDir(projectDir.path() + "/scripts");
|
||||
if (!projectScriptsDir.mkpath(".")) {
|
||||
status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto fileName = QFileInfo(avatarModelPath).fileName();
|
||||
const auto newModelPath = projectDir.absoluteFilePath(fileName);
|
||||
const auto newFSTPath = projectDir.absoluteFilePath("avatar.fst");
|
||||
QFile::copy(avatarModelPath, newModelPath);
|
||||
|
||||
QFileInfo fbxInfo{ newModelPath };
|
||||
if (!fbxInfo.exists() || !fbxInfo.isFile()) {
|
||||
status = AvatarProjectStatus::ERROR_CREATE_FIND_MODEL;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QFile fbx{ fbxInfo.filePath() };
|
||||
if (!fbx.open(QIODevice::ReadOnly)) {
|
||||
status = AvatarProjectStatus::ERROR_CREATE_OPEN_MODEL;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<hfm::Model> hfmModel;
|
||||
|
||||
try {
|
||||
const QByteArray fbxContents = fbx.readAll();
|
||||
hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), fbxInfo.filePath());
|
||||
} catch (const QString& error) {
|
||||
Q_UNUSED(error)
|
||||
status = AvatarProjectStatus::ERROR_CREATE_READ_MODEL;
|
||||
return nullptr;
|
||||
}
|
||||
QStringList textures{};
|
||||
|
||||
auto addTextureToList = [&textures](hfm::Texture texture) mutable {
|
||||
if (!texture.filename.isEmpty() && texture.content.isEmpty() && !textures.contains(texture.filename)) {
|
||||
textures << texture.filename;
|
||||
}
|
||||
};
|
||||
|
||||
foreach(const HFMMaterial material, hfmModel->materials) {
|
||||
addTextureToList(material.normalTexture);
|
||||
addTextureToList(material.albedoTexture);
|
||||
addTextureToList(material.opacityTexture);
|
||||
addTextureToList(material.glossTexture);
|
||||
addTextureToList(material.roughnessTexture);
|
||||
addTextureToList(material.specularTexture);
|
||||
addTextureToList(material.metallicTexture);
|
||||
addTextureToList(material.emissiveTexture);
|
||||
addTextureToList(material.occlusionTexture);
|
||||
addTextureToList(material.scatteringTexture);
|
||||
addTextureToList(material.lightmapTexture);
|
||||
}
|
||||
|
||||
QDir textureDir(textureFolder.isEmpty() ? fbxInfo.absoluteDir() : textureFolder);
|
||||
|
||||
for (const auto& texture : textures) {
|
||||
QString sourcePath = textureDir.path() + "/" + texture;
|
||||
QString targetPath = projectTexturesDir.path() + "/" + texture;
|
||||
|
||||
QFileInfo sourceTexturePath(sourcePath);
|
||||
if (sourceTexturePath.exists()) {
|
||||
QFile::copy(sourcePath, targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
auto fst = FST::createFSTFromModel(newFSTPath, newModelPath, *hfmModel);
|
||||
fst->setName(avatarProjectName);
|
||||
|
||||
if (!fst->write()) {
|
||||
status = AvatarProjectStatus::ERROR_CREATE_WRITE_FST;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
status = AvatarProjectStatus::SUCCESS;
|
||||
return new AvatarProject(fst);
|
||||
}
|
||||
|
||||
QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) const {
|
||||
QStringList result{};
|
||||
constexpr auto flags = QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden;
|
||||
if (!scriptsDir.exists()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const auto& script : scriptsDir.entryInfoList({}, flags)) {
|
||||
if (script.fileName().toLower().endsWith(".js")) {
|
||||
result.push_back("scripts/" + script.fileName());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool AvatarProject::isValidNewProjectName(const QString& projectPath, const QString& projectName) {
|
||||
if (projectPath.trimmed().isEmpty() || projectName.trimmed().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
QDir dir(projectPath + "/" + projectName);
|
||||
return !dir.exists();
|
||||
}
|
||||
|
||||
AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) :
|
||||
AvatarProject::AvatarProject(new FST(fstPath, FSTReader::readMapping(data))) {
|
||||
}
|
||||
AvatarProject::AvatarProject(FST* fst) {
|
||||
_fst = fst;
|
||||
auto fileInfo = QFileInfo(getFSTPath());
|
||||
_directory = fileInfo.absoluteDir();
|
||||
|
||||
_fst->setScriptPaths(getScriptPaths(QDir(_directory.path() + "/scripts")));
|
||||
_fst->write();
|
||||
|
||||
refreshProjectFiles();
|
||||
|
||||
_projectPath = fileInfo.absoluteDir().absolutePath();
|
||||
}
|
||||
|
||||
void AvatarProject::appendDirectory(const QString& prefix, const QDir& dir) {
|
||||
constexpr auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden;
|
||||
for (auto& entry : dir.entryInfoList({}, flags)) {
|
||||
if (entry.isFile()) {
|
||||
_projectFiles.append({ entry.absoluteFilePath(), prefix + entry.fileName() });
|
||||
} else if (entry.isDir()) {
|
||||
appendDirectory(prefix + entry.fileName() + "/", entry.absoluteFilePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AvatarProject::refreshProjectFiles() {
|
||||
_projectFiles.clear();
|
||||
appendDirectory("", _directory);
|
||||
}
|
||||
|
||||
QStringList AvatarProject::getProjectFiles() const {
|
||||
QStringList paths;
|
||||
for (auto& path : _projectFiles) {
|
||||
paths.append(path.relativePath);
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) {
|
||||
QUuid itemID;
|
||||
if (updateExisting) {
|
||||
itemID = _fst->getMarketplaceID();
|
||||
}
|
||||
auto uploader = new MarketplaceItemUploader(getProjectName(), "", QFileInfo(getFSTPath()).fileName(),
|
||||
itemID, _projectFiles);
|
||||
connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() {
|
||||
if (uploader->getError() == MarketplaceItemUploader::Error::None) {
|
||||
_fst->setMarketplaceID(uploader->getMarketplaceID());
|
||||
_fst->write();
|
||||
}
|
||||
});
|
||||
|
||||
return uploader;
|
||||
}
|
||||
|
||||
void AvatarProject::openInInventory() const {
|
||||
constexpr int TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS { 1000 };
|
||||
|
||||
auto tablet = dynamic_cast<TabletProxy*>(
|
||||
DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system"));
|
||||
tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml");
|
||||
DependencyManager::get<HMDScriptingInterface>()->openTablet();
|
||||
tablet->getTabletRoot()->forceActiveFocus();
|
||||
auto name = getProjectName();
|
||||
|
||||
// I'm not a fan of this, but it's the only current option.
|
||||
QTimer::singleShot(TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS, [name, tablet]() {
|
||||
tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } }));
|
||||
});
|
||||
}
|
115
interface/src/avatar/AvatarProject.h
Normal file
115
interface/src/avatar/AvatarProject.h
Normal file
|
@ -0,0 +1,115 @@
|
|||
//
|
||||
// AvatarProject.h
|
||||
//
|
||||
//
|
||||
// Created by Thijs Wenker on 12/7/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
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#ifndef hifi_AvatarProject_h
|
||||
#define hifi_AvatarProject_h
|
||||
|
||||
#include "MarketplaceItemUploader.h"
|
||||
#include "ProjectFile.h"
|
||||
#include "FST.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QDir>
|
||||
#include <QVariantHash>
|
||||
#include <QStandardPaths>
|
||||
|
||||
namespace AvatarProjectStatus {
|
||||
Q_NAMESPACE
|
||||
enum AvatarProjectStatus {
|
||||
NONE,
|
||||
SUCCESS,
|
||||
ERROR_CREATE_PROJECT_NAME,
|
||||
ERROR_CREATE_CREATING_DIRECTORIES,
|
||||
ERROR_CREATE_FIND_MODEL,
|
||||
ERROR_CREATE_OPEN_MODEL,
|
||||
ERROR_CREATE_READ_MODEL,
|
||||
ERROR_CREATE_WRITE_FST,
|
||||
ERROR_OPEN_INVALID_FILE_TYPE,
|
||||
ERROR_OPEN_PROJECT_FOLDER,
|
||||
ERROR_OPEN_FIND_FST,
|
||||
ERROR_OPEN_OPEN_FST,
|
||||
ERROR_OPEN_FIND_MODEL
|
||||
};
|
||||
Q_ENUM_NS(AvatarProjectStatus)
|
||||
}
|
||||
|
||||
|
||||
class AvatarProject : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(FST* fst READ getFST CONSTANT)
|
||||
|
||||
Q_PROPERTY(QStringList projectFiles READ getProjectFiles NOTIFY projectFilesChanged)
|
||||
|
||||
Q_PROPERTY(QString projectFolderPath READ getProjectPath CONSTANT)
|
||||
Q_PROPERTY(QString projectFSTPath READ getFSTPath CONSTANT)
|
||||
Q_PROPERTY(QString projectFBXPath READ getFBXPath CONSTANT)
|
||||
Q_PROPERTY(QString name READ getProjectName WRITE setProjectName NOTIFY nameChanged)
|
||||
|
||||
public:
|
||||
Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting);
|
||||
Q_INVOKABLE void openInInventory() const;
|
||||
Q_INVOKABLE QStringList getProjectFiles() const;
|
||||
|
||||
Q_INVOKABLE QString getProjectName() const { return _fst->getName(); }
|
||||
Q_INVOKABLE void setProjectName(const QString& newProjectName) {
|
||||
if (newProjectName.trimmed().length() > 0) {
|
||||
_fst->setName(newProjectName);
|
||||
_fst->write();
|
||||
emit nameChanged();
|
||||
}
|
||||
}
|
||||
Q_INVOKABLE QString getProjectPath() const { return _projectPath; }
|
||||
Q_INVOKABLE QString getFSTPath() const { return _fst->getPath(); }
|
||||
Q_INVOKABLE QString getFBXPath() const {
|
||||
return QDir::cleanPath(QDir(_projectPath).absoluteFilePath(_fst->getModelPath()));
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the AvatarProject or a nullptr on failure.
|
||||
*/
|
||||
static AvatarProject* openAvatarProject(const QString& path, AvatarProjectStatus::AvatarProjectStatus& status);
|
||||
static AvatarProject* createAvatarProject(const QString& projectsFolder,
|
||||
const QString& avatarProjectName,
|
||||
const QString& avatarModelPath,
|
||||
const QString& textureFolder,
|
||||
AvatarProjectStatus::AvatarProjectStatus& status);
|
||||
|
||||
static bool isValidNewProjectName(const QString& projectPath, const QString& projectName);
|
||||
|
||||
static QString getDefaultProjectsPath() {
|
||||
return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/High Fidelity Projects";
|
||||
}
|
||||
|
||||
signals:
|
||||
void nameChanged();
|
||||
void projectFilesChanged();
|
||||
|
||||
private:
|
||||
AvatarProject(const QString& fstPath, const QByteArray& data);
|
||||
AvatarProject(FST* fst);
|
||||
|
||||
~AvatarProject() { _fst->deleteLater(); }
|
||||
|
||||
FST* getFST() { return _fst; }
|
||||
|
||||
void refreshProjectFiles();
|
||||
void appendDirectory(const QString& prefix, const QDir& dir);
|
||||
QStringList getScriptPaths(const QDir& scriptsDir) const;
|
||||
|
||||
FST* _fst;
|
||||
|
||||
QDir _directory;
|
||||
QList<ProjectFilePath> _projectFiles{};
|
||||
QString _projectPath;
|
||||
};
|
||||
|
||||
#endif // hifi_AvatarProject_h
|
321
interface/src/avatar/MarketplaceItemUploader.cpp
Normal file
321
interface/src/avatar/MarketplaceItemUploader.cpp
Normal file
|
@ -0,0 +1,321 @@
|
|||
//
|
||||
// MarketplaceItemUploader.cpp
|
||||
//
|
||||
//
|
||||
// Created by Ryan Huffman on 12/10/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
|
||||
//
|
||||
|
||||
#include "MarketplaceItemUploader.h"
|
||||
|
||||
#include <AccountManager.h>
|
||||
#include <DependencyManager.h>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <quazip5/quazip.h>
|
||||
#include <quazip5/quazipfile.h>
|
||||
#endif
|
||||
|
||||
#include <QTimer>
|
||||
#include <QBuffer>
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
|
||||
MarketplaceItemUploader::MarketplaceItemUploader(QString title,
|
||||
QString description,
|
||||
QString rootFilename,
|
||||
QUuid marketplaceID,
|
||||
QList<ProjectFilePath> filePaths) :
|
||||
_title(title),
|
||||
_description(description),
|
||||
_rootFilename(rootFilename),
|
||||
_marketplaceID(marketplaceID),
|
||||
_filePaths(filePaths) {
|
||||
}
|
||||
|
||||
void MarketplaceItemUploader::setState(State newState) {
|
||||
Q_ASSERT(_state != State::Complete);
|
||||
Q_ASSERT(_error == Error::None);
|
||||
Q_ASSERT(newState != _state);
|
||||
|
||||
_state = newState;
|
||||
emit stateChanged(newState);
|
||||
if (newState == State::Complete) {
|
||||
emit completed();
|
||||
emit finishedChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void MarketplaceItemUploader::setError(Error error) {
|
||||
Q_ASSERT(_state != State::Complete);
|
||||
Q_ASSERT(_error == Error::None);
|
||||
|
||||
_error = error;
|
||||
emit errorChanged(error);
|
||||
emit finishedChanged();
|
||||
}
|
||||
|
||||
void MarketplaceItemUploader::send() {
|
||||
doGetCategories();
|
||||
}
|
||||
|
||||
void MarketplaceItemUploader::doGetCategories() {
|
||||
setState(State::GettingCategories);
|
||||
|
||||
static const QString path = "/api/v1/marketplace/categories";
|
||||
|
||||
auto accountManager = DependencyManager::get<AccountManager>();
|
||||
auto request = accountManager->createRequest(path, AccountManagerAuth::None);
|
||||
|
||||
qWarning() << "Request url is: " << request.url();
|
||||
|
||||
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
|
||||
|
||||
QNetworkReply* reply = networkAccessManager.get(request);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
auto error = reply->error();
|
||||
if (error == QNetworkReply::NoError) {
|
||||
auto doc = QJsonDocument::fromJson(reply->readAll());
|
||||
auto extractCategoryID = [&doc]() -> std::pair<bool, int> {
|
||||
auto items = doc.object()["data"].toObject()["items"];
|
||||
if (!items.isArray()) {
|
||||
qWarning() << "Categories parse error: data.items is not an array";
|
||||
return { false, 0 };
|
||||
}
|
||||
|
||||
auto itemsArray = items.toArray();
|
||||
for (const auto item : itemsArray) {
|
||||
if (!item.isObject()) {
|
||||
qWarning() << "Categories parse error: item is not an object";
|
||||
return { false, 0 };
|
||||
}
|
||||
|
||||
auto itemObject = item.toObject();
|
||||
if (itemObject["name"].toString() == "Avatars") {
|
||||
auto idValue = itemObject["id"];
|
||||
if (!idValue.isDouble()) {
|
||||
qWarning() << "Categories parse error: id is not a number";
|
||||
return { false, 0 };
|
||||
}
|
||||
return { true, (int)idValue.toDouble() };
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "Categories parse error: could not find a category for 'Avatar'";
|
||||
return { false, 0 };
|
||||
};
|
||||
|
||||
bool success;
|
||||
std::tie(success, _categoryID) = extractCategoryID();
|
||||
if (!success) {
|
||||
qWarning() << "Failed to find marketplace category id";
|
||||
setError(Error::Unknown);
|
||||
} else {
|
||||
qDebug() << "Marketplace Avatar category ID is" << _categoryID;
|
||||
doUploadAvatar();
|
||||
}
|
||||
} else {
|
||||
setError(Error::Unknown);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MarketplaceItemUploader::doUploadAvatar() {
|
||||
#ifdef Q_OS_ANDROID
|
||||
qWarning() << "Marketplace uploading is not supported on Android";
|
||||
setError(Error::Unknown);
|
||||
return;
|
||||
#else
|
||||
QBuffer buffer{ &_fileData };
|
||||
QuaZip zip{ &buffer };
|
||||
if (!zip.open(QuaZip::Mode::mdAdd)) {
|
||||
qWarning() << "Failed to open zip";
|
||||
setError(Error::Unknown);
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& filePath : _filePaths) {
|
||||
qWarning() << "Zipping: " << filePath.absolutePath << filePath.relativePath;
|
||||
QFileInfo fileInfo{ filePath.absolutePath };
|
||||
|
||||
QuaZipFile zipFile{ &zip };
|
||||
if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(filePath.relativePath))) {
|
||||
qWarning() << "Could not open zip file:" << zipFile.getZipError();
|
||||
setError(Error::Unknown);
|
||||
return;
|
||||
}
|
||||
QFile file{ filePath.absolutePath };
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
zipFile.write(file.readAll());
|
||||
} else {
|
||||
qWarning() << "Failed to open: " << filePath.absolutePath;
|
||||
}
|
||||
file.close();
|
||||
zipFile.close();
|
||||
if (zipFile.getZipError() != UNZ_OK) {
|
||||
qWarning() << "Could not close zip file: " << zipFile.getZipError();
|
||||
setState(State::Complete);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
zip.close();
|
||||
|
||||
qDebug() << "Finished zipping, size: " << (buffer.size() / (1000.0f)) << "KB";
|
||||
|
||||
QString path = "/api/v1/marketplace/items";
|
||||
bool creating = true;
|
||||
if (!_marketplaceID.isNull()) {
|
||||
creating = false;
|
||||
auto idWithBraces = _marketplaceID.toString();
|
||||
auto idWithoutBraces = idWithBraces.mid(1, idWithBraces.length() - 2);
|
||||
path += "/" + idWithoutBraces;
|
||||
}
|
||||
auto accountManager = DependencyManager::get<AccountManager>();
|
||||
auto request = accountManager->createRequest(path, AccountManagerAuth::Required);
|
||||
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
|
||||
|
||||
// TODO(huffman) add JSON escaping
|
||||
auto escapeJson = [](QString str) -> QString { return str; };
|
||||
|
||||
QString jsonString = "{\"marketplace_item\":{";
|
||||
jsonString += "\"title\":\"" + escapeJson(_title) + "\"";
|
||||
|
||||
// Items cannot have their description updated after they have been submitted.
|
||||
if (creating) {
|
||||
jsonString += ",\"description\":\"" + escapeJson(_description) + "\"";
|
||||
}
|
||||
|
||||
jsonString += ",\"root_file_key\":\"" + escapeJson(_rootFilename) + "\"";
|
||||
jsonString += ",\"category_ids\":[" + QStringLiteral("%1").arg(_categoryID) + "]";
|
||||
jsonString += ",\"license\":0";
|
||||
jsonString += ",\"files\":\"" + QString::fromLatin1(_fileData.toBase64()) + "\"}}";
|
||||
|
||||
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
|
||||
|
||||
QNetworkReply* reply{ nullptr };
|
||||
if (creating) {
|
||||
reply = networkAccessManager.post(request, jsonString.toUtf8());
|
||||
} else {
|
||||
reply = networkAccessManager.put(request, jsonString.toUtf8());
|
||||
}
|
||||
|
||||
connect(reply, &QNetworkReply::uploadProgress, this, [this](float bytesSent, float bytesTotal) {
|
||||
if (_state == State::UploadingAvatar) {
|
||||
emit uploadProgress(bytesSent, bytesTotal);
|
||||
if (bytesSent >= bytesTotal) {
|
||||
setState(State::WaitingForUploadResponse);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
_responseData = reply->readAll();
|
||||
|
||||
auto error = reply->error();
|
||||
if (error == QNetworkReply::NoError) {
|
||||
auto doc = QJsonDocument::fromJson(_responseData.toLatin1());
|
||||
auto status = doc.object()["status"].toString();
|
||||
if (status == "success") {
|
||||
_marketplaceID = QUuid::fromString(doc["data"].toObject()["marketplace_id"].toString());
|
||||
_itemVersion = doc["data"].toObject()["version"].toDouble();
|
||||
setState(State::WaitingForInventory);
|
||||
doWaitForInventory();
|
||||
} else {
|
||||
qWarning() << "Got error response while uploading avatar: " << _responseData;
|
||||
setError(Error::Unknown);
|
||||
}
|
||||
} else {
|
||||
qWarning() << "Got error while uploading avatar: " << reply->error() << reply->errorString() << _responseData;
|
||||
setError(Error::Unknown);
|
||||
}
|
||||
});
|
||||
|
||||
setState(State::UploadingAvatar);
|
||||
#endif
|
||||
}
|
||||
|
||||
void MarketplaceItemUploader::doWaitForInventory() {
|
||||
static const QString path = "/api/v1/commerce/inventory";
|
||||
|
||||
auto accountManager = DependencyManager::get<AccountManager>();
|
||||
auto request = accountManager->createRequest(path, AccountManagerAuth::Required);
|
||||
|
||||
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
|
||||
|
||||
QNetworkReply* reply = networkAccessManager.post(request, "");
|
||||
|
||||
_numRequestsForInventory++;
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
auto data = reply->readAll();
|
||||
|
||||
bool success = false;
|
||||
|
||||
auto error = reply->error();
|
||||
if (error == QNetworkReply::NoError) {
|
||||
// Parse response data
|
||||
auto doc = QJsonDocument::fromJson(data);
|
||||
auto isAssetAvailable = [this, &doc]() -> bool {
|
||||
if (!doc.isObject()) {
|
||||
return false;
|
||||
}
|
||||
auto root = doc.object();
|
||||
auto status = root["status"].toString();
|
||||
if (status != "success") {
|
||||
return false;
|
||||
}
|
||||
auto data = root["data"];
|
||||
if (!data.isObject()) {
|
||||
return false;
|
||||
}
|
||||
auto assets = data.toObject()["assets"];
|
||||
if (!assets.isArray()) {
|
||||
return false;
|
||||
}
|
||||
for (auto asset : assets.toArray()) {
|
||||
auto assetObject = asset.toObject();
|
||||
auto id = QUuid::fromString(assetObject["id"].toString());
|
||||
if (id.isNull()) {
|
||||
continue;
|
||||
}
|
||||
if (id == _marketplaceID) {
|
||||
auto version = assetObject["version"];
|
||||
auto valid = assetObject["valid"];
|
||||
if (version.isDouble() && valid.isBool()) {
|
||||
if ((int)version.toDouble() >= _itemVersion && valid.toBool()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
success = isAssetAvailable();
|
||||
}
|
||||
if (success) {
|
||||
qDebug() << "Found item in inventory";
|
||||
setState(State::Complete);
|
||||
} else {
|
||||
constexpr int MAX_INVENTORY_REQUESTS { 8 };
|
||||
constexpr int TIME_BETWEEN_INVENTORY_REQUESTS_MS { 5000 };
|
||||
if (_numRequestsForInventory > MAX_INVENTORY_REQUESTS) {
|
||||
qDebug() << "Failed to find item in inventory";
|
||||
setError(Error::Unknown);
|
||||
} else {
|
||||
QTimer::singleShot(TIME_BETWEEN_INVENTORY_REQUESTS_MS, [this]() { doWaitForInventory(); });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
105
interface/src/avatar/MarketplaceItemUploader.h
Normal file
105
interface/src/avatar/MarketplaceItemUploader.h
Normal file
|
@ -0,0 +1,105 @@
|
|||
//
|
||||
// MarketplaceItemUploader.h
|
||||
//
|
||||
//
|
||||
// Created by Ryan Huffman on 12/10/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
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#ifndef hifi_MarketplaceItemUploader_h
|
||||
#define hifi_MarketplaceItemUploader_h
|
||||
|
||||
#include "ProjectFile.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QUuid>
|
||||
|
||||
class QNetworkReply;
|
||||
|
||||
class MarketplaceItemUploader : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(bool finished READ getFinished NOTIFY finishedChanged)
|
||||
|
||||
Q_PROPERTY(bool complete READ getComplete NOTIFY stateChanged)
|
||||
Q_PROPERTY(State state READ getState NOTIFY stateChanged)
|
||||
Q_PROPERTY(Error error READ getError NOTIFY errorChanged)
|
||||
Q_PROPERTY(QString responseData READ getResponseData)
|
||||
public:
|
||||
enum class Error {
|
||||
None,
|
||||
Unknown,
|
||||
};
|
||||
Q_ENUM(Error);
|
||||
|
||||
enum class State {
|
||||
Idle,
|
||||
GettingCategories,
|
||||
UploadingAvatar,
|
||||
WaitingForUploadResponse,
|
||||
WaitingForInventory,
|
||||
Complete,
|
||||
};
|
||||
Q_ENUM(State);
|
||||
|
||||
MarketplaceItemUploader(QString title,
|
||||
QString description,
|
||||
QString rootFilename,
|
||||
QUuid marketplaceID,
|
||||
QList<ProjectFilePath> filePaths);
|
||||
|
||||
Q_INVOKABLE void send();
|
||||
|
||||
void setError(Error error);
|
||||
|
||||
QString getResponseData() const { return _responseData; }
|
||||
void setState(State newState);
|
||||
State getState() const { return _state; }
|
||||
bool getComplete() const { return _state == State::Complete; }
|
||||
|
||||
QUuid getMarketplaceID() const { return _marketplaceID; }
|
||||
|
||||
Error getError() const { return _error; }
|
||||
bool getFinished() const { return _state == State::Complete || _error != Error::None; }
|
||||
|
||||
signals:
|
||||
void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
|
||||
void completed();
|
||||
|
||||
void stateChanged(State newState);
|
||||
void errorChanged(Error error);
|
||||
|
||||
// Triggered when the upload has finished, either succesfully completing, or stopping with an error
|
||||
void finishedChanged();
|
||||
|
||||
private:
|
||||
void doGetCategories();
|
||||
void doUploadAvatar();
|
||||
void doWaitForInventory();
|
||||
|
||||
QNetworkReply* _reply;
|
||||
|
||||
State _state { State::Idle };
|
||||
Error _error { Error::None };
|
||||
|
||||
QString _title;
|
||||
QString _description;
|
||||
QString _rootFilename;
|
||||
QUuid _marketplaceID;
|
||||
int _categoryID;
|
||||
int _itemVersion;
|
||||
|
||||
QString _responseData;
|
||||
|
||||
int _numRequestsForInventory { 0 };
|
||||
|
||||
QString _rootFilePath;
|
||||
QList<ProjectFilePath> _filePaths;
|
||||
QByteArray _fileData;
|
||||
};
|
||||
|
||||
#endif // hifi_MarketplaceItemUploader_h
|
|
@ -50,6 +50,7 @@
|
|||
#include <RecordingScriptingInterface.h>
|
||||
#include <trackers/FaceTracker.h>
|
||||
#include <RenderableModelEntityItem.h>
|
||||
#include <VariantMapToScriptValue.h>
|
||||
|
||||
#include "MyHead.h"
|
||||
#include "MySkeletonModel.h"
|
||||
|
@ -281,6 +282,8 @@ MyAvatar::MyAvatar(QThread* thread) :
|
|||
|
||||
MyAvatar::~MyAvatar() {
|
||||
_lookAtTargetAvatar.reset();
|
||||
delete _myScriptEngine;
|
||||
_myScriptEngine = nullptr;
|
||||
}
|
||||
|
||||
void MyAvatar::setDominantHand(const QString& hand) {
|
||||
|
@ -671,7 +674,7 @@ void MyAvatar::update(float deltaTime) {
|
|||
|
||||
_clientTraitsHandler->sendChangedTraitsToMixer();
|
||||
|
||||
simulate(deltaTime);
|
||||
simulate(deltaTime, true);
|
||||
|
||||
currentEnergy += energyChargeRate;
|
||||
currentEnergy -= getAccelerationEnergy();
|
||||
|
@ -743,7 +746,7 @@ void MyAvatar::updateChildCauterization(SpatiallyNestablePointer object, bool ca
|
|||
}
|
||||
}
|
||||
|
||||
void MyAvatar::simulate(float deltaTime) {
|
||||
void MyAvatar::simulate(float deltaTime, bool inView) {
|
||||
PerformanceTimer perfTimer("simulate");
|
||||
animateScaleChanges(deltaTime);
|
||||
|
||||
|
@ -887,7 +890,7 @@ void MyAvatar::simulate(float deltaTime) {
|
|||
_characterController.setCollisionlessAllowed(collisionlessAllowed);
|
||||
}
|
||||
|
||||
updateAvatarEntities();
|
||||
handleChangedAvatarEntityData();
|
||||
|
||||
updateFadingStatus();
|
||||
}
|
||||
|
@ -1251,7 +1254,7 @@ void MyAvatar::saveAvatarUrl() {
|
|||
}
|
||||
}
|
||||
|
||||
void MyAvatar::resizeAvatarEntitySettingHandles(unsigned int avatarEntityIndex) {
|
||||
void MyAvatar::resizeAvatarEntitySettingHandles(uint32_t maxIndex) {
|
||||
// The original Settings interface saved avatar-entity array data like this:
|
||||
// Avatar/avatarEntityData/size: 5
|
||||
// Avatar/avatarEntityData/1/id: ...
|
||||
|
@ -1261,14 +1264,15 @@ void MyAvatar::resizeAvatarEntitySettingHandles(unsigned int avatarEntityIndex)
|
|||
// Avatar/avatarEntityData/5/properties: ...
|
||||
//
|
||||
// Create Setting::Handles to mimic this.
|
||||
|
||||
while (_avatarEntityIDSettings.size() <= avatarEntityIndex) {
|
||||
uint32_t settingsIndex = (uint32_t)_avatarEntityIDSettings.size() + 1;
|
||||
while (settingsIndex <= maxIndex) {
|
||||
Setting::Handle<QUuid> idHandle(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "avatarEntityData"
|
||||
<< QString::number(avatarEntityIndex + 1) << "id", QUuid());
|
||||
<< QString::number(settingsIndex) << "id", QUuid());
|
||||
_avatarEntityIDSettings.push_back(idHandle);
|
||||
Setting::Handle<QByteArray> dataHandle(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "avatarEntityData"
|
||||
<< QString::number(avatarEntityIndex + 1) << "properties", QByteArray());
|
||||
<< QString::number(settingsIndex) << "properties", QByteArray());
|
||||
_avatarEntityDataSettings.push_back(dataHandle);
|
||||
settingsIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1298,26 +1302,54 @@ void MyAvatar::saveData() {
|
|||
_flyingHMDSetting.set(getFlyingHMDPref());
|
||||
|
||||
auto hmdInterface = DependencyManager::get<HMDScriptingInterface>();
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
QList<QUuid> avatarEntityIDs = _avatarEntityData.keys();
|
||||
unsigned int avatarEntityCount = avatarEntityIDs.size();
|
||||
unsigned int previousAvatarEntityCount = _avatarEntityCountSetting.get(0);
|
||||
resizeAvatarEntitySettingHandles(std::max<unsigned int>(avatarEntityCount, previousAvatarEntityCount));
|
||||
_avatarEntityCountSetting.set(avatarEntityCount);
|
||||
saveAvatarEntityDataToSettings();
|
||||
}
|
||||
|
||||
unsigned int avatarEntityIndex = 0;
|
||||
for (auto entityID : avatarEntityIDs) {
|
||||
_avatarEntityIDSettings[avatarEntityIndex].set(entityID);
|
||||
_avatarEntityDataSettings[avatarEntityIndex].set(_avatarEntityData.value(entityID));
|
||||
avatarEntityIndex++;
|
||||
}
|
||||
void MyAvatar::saveAvatarEntityDataToSettings() {
|
||||
if (!_needToSaveAvatarEntitySettings) {
|
||||
return;
|
||||
}
|
||||
bool success = updateStaleAvatarEntityBlobs();
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
_needToSaveAvatarEntitySettings = false;
|
||||
|
||||
// clean up any left-over (due to the list shrinking) slots
|
||||
for (; avatarEntityIndex < previousAvatarEntityCount; avatarEntityIndex++) {
|
||||
_avatarEntityIDSettings[avatarEntityIndex].remove();
|
||||
_avatarEntityDataSettings[avatarEntityIndex].remove();
|
||||
uint32_t numEntities = (uint32_t)_cachedAvatarEntityBlobs.size();
|
||||
uint32_t prevNumEntities = _avatarEntityCountSetting.get(0);
|
||||
resizeAvatarEntitySettingHandles(std::max<uint32_t>(numEntities, prevNumEntities));
|
||||
|
||||
// save new Settings
|
||||
if (numEntities > 0) {
|
||||
// save all unfortunately-formatted-binary-blobs to Settings
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
uint32_t i = 0;
|
||||
AvatarEntityMap::const_iterator itr = _cachedAvatarEntityBlobs.begin();
|
||||
while (itr != _cachedAvatarEntityBlobs.end()) {
|
||||
_avatarEntityIDSettings[i].set(itr.key());
|
||||
_avatarEntityDataSettings[i].set(itr.value());
|
||||
++itr;
|
||||
++i;
|
||||
}
|
||||
numEntities = i;
|
||||
});
|
||||
}
|
||||
_avatarEntityCountSetting.set(numEntities);
|
||||
|
||||
// remove old Settings if any
|
||||
if (numEntities < prevNumEntities) {
|
||||
uint32_t numEntitiesToRemove = prevNumEntities - numEntities;
|
||||
for (uint32_t i = 0; i < numEntitiesToRemove; ++i) {
|
||||
if (_avatarEntityIDSettings.size() > numEntities) {
|
||||
_avatarEntityIDSettings.back().remove();
|
||||
_avatarEntityIDSettings.pop_back();
|
||||
}
|
||||
if (_avatarEntityDataSettings.size() > numEntities) {
|
||||
_avatarEntityDataSettings.back().remove();
|
||||
_avatarEntityDataSettings.pop_back();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
float loadSetting(Settings& settings, const QString& name, float defaultValue) {
|
||||
|
@ -1414,7 +1446,410 @@ void MyAvatar::setEnableInverseKinematics(bool isEnabled) {
|
|||
_skeletonModel->getRig().setEnableInverseKinematics(isEnabled);
|
||||
}
|
||||
|
||||
void MyAvatar::storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) {
|
||||
AvatarData::storeAvatarEntityDataPayload(entityID, payload);
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
_cachedAvatarEntityBlobsToAddOrUpdate.push_back(entityID);
|
||||
});
|
||||
}
|
||||
|
||||
void MyAvatar::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree) {
|
||||
AvatarData::clearAvatarEntity(entityID, requiresRemovalFromTree);
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
_cachedAvatarEntityBlobsToDelete.push_back(entityID);
|
||||
});
|
||||
}
|
||||
|
||||
void MyAvatar::sanitizeAvatarEntityProperties(EntityItemProperties& properties) const {
|
||||
properties.setEntityHostType(entity::HostType::AVATAR);
|
||||
properties.setOwningAvatarID(getID());
|
||||
|
||||
// there's no entity-server to tell us we're the simulation owner, so always set the
|
||||
// simulationOwner to the owningAvatarID and a high priority.
|
||||
properties.setSimulationOwner(getID(), AVATAR_ENTITY_SIMULATION_PRIORITY);
|
||||
|
||||
if (properties.getParentID() == AVATAR_SELF_ID) {
|
||||
properties.setParentID(getID());
|
||||
}
|
||||
|
||||
// When grabbing avatar entities, they are parented to the joint moving them, then when un-grabbed
|
||||
// they go back to the default parent (null uuid). When un-gripped, others saw the entity disappear.
|
||||
// The thinking here is the local position was noticed as changing, but not the parentID (since it is now
|
||||
// back to the default), and the entity flew off somewhere. Marking all changed definitely fixes this,
|
||||
// and seems safe (per Seth).
|
||||
properties.markAllChanged();
|
||||
}
|
||||
|
||||
void MyAvatar::handleChangedAvatarEntityData() {
|
||||
// NOTE: this is a per-frame update
|
||||
if (getID().isNull() ||
|
||||
getID() == AVATAR_SELF_ID ||
|
||||
DependencyManager::get<NodeList>()->getSessionUUID() == QUuid()) {
|
||||
// wait until MyAvatar and this Node gets an ID before doing this. Otherwise, various things go wrong:
|
||||
// things get their parent fixed up from AVATAR_SELF_ID to a null uuid which means "no parent".
|
||||
return;
|
||||
}
|
||||
if (_reloadAvatarEntityDataFromSettings) {
|
||||
loadAvatarEntityDataFromSettings();
|
||||
}
|
||||
|
||||
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
||||
if (!entityTree) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We collect changes to AvatarEntities and then handle them all in one spot per frame: handleChangedAvatarEntityData().
|
||||
// Basically this is a "transaction pattern" with an extra complication: these changes can come from two
|
||||
// "directions" and the "authoritative source" of each direction is different, so we maintain two distinct sets
|
||||
// of transaction lists:
|
||||
//
|
||||
// The _entitiesToDelete/Add/Update lists are for changes whose "authoritative sources" are already
|
||||
// correctly stored in _cachedAvatarEntityBlobs. These come from loadAvatarEntityDataFromSettings() and
|
||||
// setAvatarEntityData(). These changes need to be extracted from _cachedAvatarEntityBlobs and applied to
|
||||
// real EntityItems.
|
||||
//
|
||||
// The _cachedAvatarEntityBlobsToDelete/Add/Update lists are for changes whose "authoritative sources" are
|
||||
// already reflected in real EntityItems. These changes need to be propagated to _cachedAvatarEntityBlobs
|
||||
// and eventually to Settings.
|
||||
//
|
||||
// The DELETEs also need to be propagated to the traits, which will eventually propagate to
|
||||
// AvatarData::_packedAvatarEntityData via deeper logic.
|
||||
|
||||
// move the lists to minimize lock time
|
||||
std::vector<QUuid> cachedBlobsToDelete;
|
||||
std::vector<QUuid> cachedBlobsToUpdate;
|
||||
std::vector<QUuid> entitiesToDelete;
|
||||
std::vector<QUuid> entitiesToAdd;
|
||||
std::vector<QUuid> entitiesToUpdate;
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
cachedBlobsToDelete = std::move(_cachedAvatarEntityBlobsToDelete);
|
||||
cachedBlobsToUpdate = std::move(_cachedAvatarEntityBlobsToAddOrUpdate);
|
||||
entitiesToDelete = std::move(_entitiesToDelete);
|
||||
entitiesToAdd = std::move(_entitiesToAdd);
|
||||
entitiesToUpdate = std::move(_entitiesToUpdate);
|
||||
});
|
||||
|
||||
auto removeAllInstancesHelper = [] (const QUuid& id, std::vector<QUuid>& v) {
|
||||
uint32_t i = 0;
|
||||
while (i < v.size()) {
|
||||
if (id == v[i]) {
|
||||
v[i] = v.back();
|
||||
v.pop_back();
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// remove delete-add and delete-update overlap
|
||||
for (const auto& id : entitiesToDelete) {
|
||||
removeAllInstancesHelper(id, cachedBlobsToUpdate);
|
||||
removeAllInstancesHelper(id, entitiesToAdd);
|
||||
removeAllInstancesHelper(id, entitiesToUpdate);
|
||||
}
|
||||
for (const auto& id : cachedBlobsToDelete) {
|
||||
removeAllInstancesHelper(id, entitiesToUpdate);
|
||||
removeAllInstancesHelper(id, cachedBlobsToUpdate);
|
||||
}
|
||||
for (const auto& id : entitiesToAdd) {
|
||||
removeAllInstancesHelper(id, entitiesToUpdate);
|
||||
}
|
||||
|
||||
// DELETE real entities
|
||||
for (const auto& id : entitiesToDelete) {
|
||||
entityTree->withWriteLock([&] {
|
||||
entityTree->deleteEntity(id);
|
||||
});
|
||||
}
|
||||
|
||||
// ADD real entities
|
||||
EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender();
|
||||
for (const auto& id : entitiesToAdd) {
|
||||
bool blobFailed = false;
|
||||
EntityItemProperties properties;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
AvatarEntityMap::iterator itr = _cachedAvatarEntityBlobs.find(id);
|
||||
if (itr == _cachedAvatarEntityBlobs.end()) {
|
||||
blobFailed = true; // blob doesn't exist
|
||||
return;
|
||||
}
|
||||
if (!EntityItemProperties::blobToProperties(*_myScriptEngine, itr.value(), properties)) {
|
||||
blobFailed = true; // blob is corrupt
|
||||
}
|
||||
});
|
||||
if (blobFailed) {
|
||||
// remove from _cachedAvatarEntityBlobUpdatesToSkip just in case:
|
||||
// avoids a resource leak when blob updates to be skipped are never actually skipped
|
||||
// when the blob fails to result in a real EntityItem
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
removeAllInstancesHelper(id, _cachedAvatarEntityBlobUpdatesToSkip);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
sanitizeAvatarEntityProperties(properties);
|
||||
entityTree->withWriteLock([&] {
|
||||
EntityItemPointer entity = entityTree->addEntity(id, properties);
|
||||
if (entity) {
|
||||
packetSender->queueEditEntityMessage(PacketType::EntityAdd, entityTree, id, properties);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// CHANGE real entities
|
||||
for (const auto& id : entitiesToUpdate) {
|
||||
EntityItemProperties properties;
|
||||
bool skip = false;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
AvatarEntityMap::iterator itr = _cachedAvatarEntityBlobs.find(id);
|
||||
if (itr == _cachedAvatarEntityBlobs.end()) {
|
||||
skip = true;
|
||||
return;
|
||||
}
|
||||
if (!EntityItemProperties::blobToProperties(*_myScriptEngine, itr.value(), properties)) {
|
||||
skip = true;
|
||||
}
|
||||
});
|
||||
sanitizeAvatarEntityProperties(properties);
|
||||
entityTree->withWriteLock([&] {
|
||||
entityTree->updateEntity(id, properties);
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE cached blobs
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
for (const auto& id : cachedBlobsToDelete) {
|
||||
AvatarEntityMap::iterator itr = _cachedAvatarEntityBlobs.find(id);
|
||||
// remove blob and remember to remove from settings
|
||||
if (itr != _cachedAvatarEntityBlobs.end()) {
|
||||
_cachedAvatarEntityBlobs.erase(itr);
|
||||
_needToSaveAvatarEntitySettings = true;
|
||||
}
|
||||
// also remove from list of stale blobs to avoid failed entity lookup later
|
||||
std::set<QUuid>::iterator blobItr = _staleCachedAvatarEntityBlobs.find(id);
|
||||
if (blobItr != _staleCachedAvatarEntityBlobs.end()) {
|
||||
_staleCachedAvatarEntityBlobs.erase(blobItr);
|
||||
}
|
||||
// also remove from _cachedAvatarEntityBlobUpdatesToSkip just in case:
|
||||
// avoids a resource leak when things are deleted before they could be skipped
|
||||
removeAllInstancesHelper(id, _cachedAvatarEntityBlobUpdatesToSkip);
|
||||
}
|
||||
});
|
||||
|
||||
// ADD/UPDATE cached blobs
|
||||
for (const auto& id : cachedBlobsToUpdate) {
|
||||
// computing the blobs is expensive and we want to avoid it when possible
|
||||
// so we add these ids to _staleCachedAvatarEntityBlobs for later
|
||||
// and only build the blobs when absolutely necessary
|
||||
bool skip = false;
|
||||
uint32_t i = 0;
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
while (i < _cachedAvatarEntityBlobUpdatesToSkip.size()) {
|
||||
if (id == _cachedAvatarEntityBlobUpdatesToSkip[i]) {
|
||||
_cachedAvatarEntityBlobUpdatesToSkip[i] = _cachedAvatarEntityBlobUpdatesToSkip.back();
|
||||
_cachedAvatarEntityBlobUpdatesToSkip.pop_back();
|
||||
skip = true;
|
||||
break; // assume no duplicates
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!skip) {
|
||||
_staleCachedAvatarEntityBlobs.insert(id);
|
||||
_needToSaveAvatarEntitySettings = true;
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE traits
|
||||
// (no need to worry about the ADDs and UPDATEs: each will be handled when the interface
|
||||
// tries to send a real update packet (via AvatarData::storeAvatarEntityDataPayload()))
|
||||
if (_clientTraitsHandler) {
|
||||
// we have a client traits handler
|
||||
// flag removed entities as deleted so that changes are sent next frame
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
for (const auto& id : entitiesToDelete) {
|
||||
if (_packedAvatarEntityData.find(id) != _packedAvatarEntityData.end()) {
|
||||
_clientTraitsHandler->markInstancedTraitDeleted(AvatarTraits::AvatarEntity, id);
|
||||
}
|
||||
}
|
||||
for (const auto& id : cachedBlobsToDelete) {
|
||||
if (_packedAvatarEntityData.find(id) != _packedAvatarEntityData.end()) {
|
||||
_clientTraitsHandler->markInstancedTraitDeleted(AvatarTraits::AvatarEntity, id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool MyAvatar::updateStaleAvatarEntityBlobs() const {
|
||||
// call this right before you actually need to use the blobs
|
||||
//
|
||||
// Note: this method is const (and modifies mutable data members)
|
||||
// so we can call it at the Last Minute inside other const methods
|
||||
//
|
||||
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
||||
if (!entityTree) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::set<QUuid> staleBlobs = std::move(_staleCachedAvatarEntityBlobs);
|
||||
int32_t numFound = 0;
|
||||
for (const auto& id : staleBlobs) {
|
||||
bool found = false;
|
||||
EntityItemProperties properties;
|
||||
entityTree->withReadLock([&] {
|
||||
EntityItemPointer entity = entityTree->findEntityByID(id);
|
||||
if (entity) {
|
||||
properties = entity->getProperties();
|
||||
found = true;
|
||||
}
|
||||
});
|
||||
if (found) {
|
||||
++numFound;
|
||||
QByteArray blob;
|
||||
EntityItemProperties::propertiesToBlob(*_myScriptEngine, getID(), properties, blob);
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
_cachedAvatarEntityBlobs[id] = blob;
|
||||
});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MyAvatar::prepareAvatarEntityDataForReload() {
|
||||
saveAvatarEntityDataToSettings();
|
||||
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
_packedAvatarEntityData.clear();
|
||||
_entitiesToDelete.clear();
|
||||
_entitiesToAdd.clear();
|
||||
_entitiesToUpdate.clear();
|
||||
_cachedAvatarEntityBlobs.clear();
|
||||
_cachedAvatarEntityBlobsToDelete.clear();
|
||||
_cachedAvatarEntityBlobsToAddOrUpdate.clear();
|
||||
_cachedAvatarEntityBlobUpdatesToSkip.clear();
|
||||
});
|
||||
|
||||
_reloadAvatarEntityDataFromSettings = true;
|
||||
}
|
||||
|
||||
AvatarEntityMap MyAvatar::getAvatarEntityData() const {
|
||||
// NOTE: the return value is expected to be a map of unfortunately-formatted-binary-blobs
|
||||
updateStaleAvatarEntityBlobs();
|
||||
AvatarEntityMap result;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
result = _cachedAvatarEntityBlobs;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void MyAvatar::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
|
||||
// Note: this is an invokable Script call
|
||||
// avatarEntityData is expected to be a map of QByteArrays that represent EntityItemProperties objects from JavaScript,
|
||||
// aka: unfortunately-formatted-binary-blobs because we store them in non-human-readable format in Settings.
|
||||
//
|
||||
if (avatarEntityData.size() > MAX_NUM_AVATAR_ENTITIES) {
|
||||
// the data is suspect
|
||||
qCDebug(interfaceapp) << "discard suspect AvatarEntityData with size =" << avatarEntityData.size();
|
||||
return;
|
||||
}
|
||||
|
||||
// this overwrites ALL AvatarEntityData so we clear pending operations
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
_packedAvatarEntityData.clear();
|
||||
_entitiesToDelete.clear();
|
||||
_entitiesToAdd.clear();
|
||||
_entitiesToUpdate.clear();
|
||||
});
|
||||
_needToSaveAvatarEntitySettings = true;
|
||||
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
// find new and updated IDs
|
||||
AvatarEntityMap::const_iterator constItr = avatarEntityData.begin();
|
||||
while (constItr != avatarEntityData.end()) {
|
||||
QUuid id = constItr.key();
|
||||
if (_cachedAvatarEntityBlobs.find(id) == _cachedAvatarEntityBlobs.end()) {
|
||||
_entitiesToAdd.push_back(id);
|
||||
} else {
|
||||
_entitiesToUpdate.push_back(id);
|
||||
}
|
||||
++constItr;
|
||||
}
|
||||
// find and erase deleted IDs from _cachedAvatarEntityBlobs
|
||||
std::vector<QUuid> deletedIDs;
|
||||
AvatarEntityMap::iterator itr = _cachedAvatarEntityBlobs.begin();
|
||||
while (itr != _cachedAvatarEntityBlobs.end()) {
|
||||
QUuid id = itr.key();
|
||||
if (std::find(_entitiesToUpdate.begin(), _entitiesToUpdate.end(), id) == _entitiesToUpdate.end()) {
|
||||
deletedIDs.push_back(id);
|
||||
itr = _cachedAvatarEntityBlobs.erase(itr);
|
||||
} else {
|
||||
++itr;
|
||||
}
|
||||
}
|
||||
// copy new data
|
||||
constItr = avatarEntityData.begin();
|
||||
while (constItr != avatarEntityData.end()) {
|
||||
_cachedAvatarEntityBlobs.insert(constItr.key(), constItr.value());
|
||||
++constItr;
|
||||
}
|
||||
// erase deleted IDs from _packedAvatarEntityData
|
||||
for (const auto& id : deletedIDs) {
|
||||
itr = _packedAvatarEntityData.find(id);
|
||||
if (itr != _packedAvatarEntityData.end()) {
|
||||
_packedAvatarEntityData.erase(itr);
|
||||
} else {
|
||||
++itr;
|
||||
}
|
||||
_entitiesToDelete.push_back(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MyAvatar::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) {
|
||||
// NOTE: this is an invokable Script call
|
||||
// TODO: we should handle the case where entityData is corrupt or invalid
|
||||
// BEFORE we store into _cachedAvatarEntityBlobs
|
||||
_needToSaveAvatarEntitySettings = true;
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
AvatarEntityMap::iterator itr = _cachedAvatarEntityBlobs.find(entityID);
|
||||
if (itr != _cachedAvatarEntityBlobs.end()) {
|
||||
_entitiesToUpdate.push_back(entityID);
|
||||
itr.value() = entityData;
|
||||
} else {
|
||||
_entitiesToAdd.push_back(entityID);
|
||||
_cachedAvatarEntityBlobs.insert(entityID, entityData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MyAvatar::avatarEntityDataToJson(QJsonObject& root) const {
|
||||
updateStaleAvatarEntityBlobs();
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
if (!_cachedAvatarEntityBlobs.empty()) {
|
||||
QJsonArray avatarEntityJson;
|
||||
int entityCount = 0;
|
||||
AvatarEntityMap::const_iterator itr = _cachedAvatarEntityBlobs.begin();
|
||||
while (itr != _cachedAvatarEntityBlobs.end()) {
|
||||
QVariantMap entityData;
|
||||
QUuid id = _avatarEntityForRecording.size() == _cachedAvatarEntityBlobs.size() ? _avatarEntityForRecording.values()[entityCount++] : itr.key();
|
||||
entityData.insert("id", id);
|
||||
entityData.insert("properties", itr.value().toBase64());
|
||||
avatarEntityJson.push_back(QVariant(entityData).toJsonObject());
|
||||
++itr;
|
||||
}
|
||||
const QString JSON_AVATAR_ENTITIES = QStringLiteral("attachedEntities");
|
||||
root[JSON_AVATAR_ENTITIES] = avatarEntityJson;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MyAvatar::loadData() {
|
||||
if (!_myScriptEngine) {
|
||||
_myScriptEngine = new QScriptEngine();
|
||||
}
|
||||
getHead()->setBasePitch(_headPitchSetting.get());
|
||||
|
||||
_yawSpeed = _yawSpeedSetting.get(_yawSpeed);
|
||||
|
@ -1426,14 +1861,7 @@ void MyAvatar::loadData() {
|
|||
|
||||
useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName);
|
||||
|
||||
int avatarEntityCount = _avatarEntityCountSetting.get(0);
|
||||
for (int i = 0; i < avatarEntityCount; i++) {
|
||||
resizeAvatarEntitySettingHandles(i);
|
||||
// QUuid entityID = QUuid::createUuid(); // generate a new ID
|
||||
QUuid entityID = _avatarEntityIDSettings[i].get(QUuid());
|
||||
QByteArray properties = _avatarEntityDataSettings[i].get();
|
||||
updateAvatarEntity(entityID, properties);
|
||||
}
|
||||
loadAvatarEntityDataFromSettings();
|
||||
|
||||
// Flying preferences must be loaded before calling setFlyingEnabled()
|
||||
Setting::Handle<bool> firstRunVal { Settings::firstRun, true };
|
||||
|
@ -1455,6 +1883,38 @@ void MyAvatar::loadData() {
|
|||
setEnableDebugDrawPosition(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawPosition));
|
||||
}
|
||||
|
||||
void MyAvatar::loadAvatarEntityDataFromSettings() {
|
||||
// this overwrites ALL AvatarEntityData so we clear pending operations
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
_packedAvatarEntityData.clear();
|
||||
_entitiesToDelete.clear();
|
||||
_entitiesToAdd.clear();
|
||||
_entitiesToUpdate.clear();
|
||||
});
|
||||
_reloadAvatarEntityDataFromSettings = false;
|
||||
_needToSaveAvatarEntitySettings = false;
|
||||
|
||||
int numEntities = _avatarEntityCountSetting.get(0);
|
||||
if (numEntities == 0) {
|
||||
return;
|
||||
}
|
||||
resizeAvatarEntitySettingHandles(numEntities);
|
||||
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
_entitiesToAdd.reserve(numEntities);
|
||||
// TODO: build map between old and new IDs so we can restitch parent-child relationships
|
||||
for (int i = 0; i < numEntities; i++) {
|
||||
QUuid id = QUuid::createUuid(); // generate a new ID
|
||||
_cachedAvatarEntityBlobs[id] = _avatarEntityDataSettings[i].get();
|
||||
_entitiesToAdd.push_back(id);
|
||||
// this blob is the "authoritative source" for this AvatarEntity and we want to avoid overwriting it
|
||||
// (the outgoing update packet will flag it for save-back into the blob)
|
||||
// which is why we remember its id: to skip its save-back later
|
||||
_cachedAvatarEntityBlobUpdatesToSkip.push_back(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MyAvatar::saveAttachmentData(const AttachmentData& attachment) const {
|
||||
Settings settings;
|
||||
settings.beginGroup("savedAttachmentData");
|
||||
|
@ -1531,35 +1991,74 @@ ScriptAvatarData* MyAvatar::getTargetAvatar() const {
|
|||
}
|
||||
}
|
||||
|
||||
void MyAvatar::updateLookAtTargetAvatar() {
|
||||
//
|
||||
// Look at the avatar whose eyes are closest to the ray in direction of my avatar's head
|
||||
// And set the correctedLookAt for all (nearby) avatars that are looking at me.
|
||||
_lookAtTargetAvatar.reset();
|
||||
_targetAvatarPosition = glm::vec3(0.0f);
|
||||
static float lookAtCostFunction(const glm::vec3& myForward, const glm::vec3& myPosition, const glm::vec3& otherForward, const glm::vec3& otherPosition,
|
||||
bool otherIsTalking, bool lookingAtOtherAlready) {
|
||||
const float DISTANCE_FACTOR = 3.14f;
|
||||
const float MY_ANGLE_FACTOR = 1.0f;
|
||||
const float OTHER_ANGLE_FACTOR = 1.0f;
|
||||
const float OTHER_IS_TALKING_TERM = otherIsTalking ? 1.0f : 0.0f;
|
||||
const float LOOKING_AT_OTHER_ALREADY_TERM = lookingAtOtherAlready ? -0.2f : 0.0f;
|
||||
|
||||
glm::vec3 lookForward = getHead()->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD;
|
||||
glm::vec3 cameraPosition = qApp->getCamera().getPosition();
|
||||
const float GREATEST_LOOKING_AT_DISTANCE = 10.0f; // meters
|
||||
const float MAX_MY_ANGLE = PI / 8.0f; // 22.5 degrees, Don't look too far away from the head facing.
|
||||
const float MAX_OTHER_ANGLE = (3.0f * PI) / 4.0f; // 135 degrees, Don't stare at the back of another avatars head.
|
||||
|
||||
float smallestAngleTo = glm::radians(DEFAULT_FIELD_OF_VIEW_DEGREES) / 2.0f;
|
||||
const float KEEP_LOOKING_AT_CURRENT_ANGLE_FACTOR = 1.3f;
|
||||
const float GREATEST_LOOKING_AT_DISTANCE = 10.0f;
|
||||
glm::vec3 d = otherPosition - myPosition;
|
||||
float distance = glm::length(d);
|
||||
glm::vec3 dUnit = d / distance;
|
||||
float myAngle = acosf(glm::dot(myForward, dUnit));
|
||||
float otherAngle = acosf(glm::dot(otherForward, -dUnit));
|
||||
|
||||
AvatarHash hash = DependencyManager::get<AvatarManager>()->getHashCopy();
|
||||
if (distance > GREATEST_LOOKING_AT_DISTANCE || myAngle > MAX_MY_ANGLE || otherAngle > MAX_OTHER_ANGLE) {
|
||||
return FLT_MAX;
|
||||
} else {
|
||||
return (DISTANCE_FACTOR * distance +
|
||||
MY_ANGLE_FACTOR * myAngle +
|
||||
OTHER_ANGLE_FACTOR * otherAngle +
|
||||
OTHER_IS_TALKING_TERM +
|
||||
LOOKING_AT_OTHER_ALREADY_TERM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (const AvatarSharedPointer& avatarPointer, hash) {
|
||||
auto avatar = static_pointer_cast<Avatar>(avatarPointer);
|
||||
bool isCurrentTarget = avatar->getIsLookAtTarget();
|
||||
float distanceTo = glm::length(avatar->getHead()->getEyePosition() - cameraPosition);
|
||||
avatar->setIsLookAtTarget(false);
|
||||
if (!avatar->isMyAvatar() && avatar->isInitialized() &&
|
||||
(distanceTo < GREATEST_LOOKING_AT_DISTANCE * getModelScale())) {
|
||||
float radius = glm::length(avatar->getHead()->getEyePosition() - avatar->getHead()->getRightEyePosition());
|
||||
float angleTo = coneSphereAngle(getHead()->getEyePosition(), lookForward, avatar->getHead()->getEyePosition(), radius);
|
||||
if (angleTo < (smallestAngleTo * (isCurrentTarget ? KEEP_LOOKING_AT_CURRENT_ANGLE_FACTOR : 1.0f))) {
|
||||
_lookAtTargetAvatar = avatarPointer;
|
||||
_targetAvatarPosition = avatarPointer->getWorldPosition();
|
||||
void MyAvatar::computeMyLookAtTarget(const AvatarHash& hash) {
|
||||
glm::vec3 myForward = getHead()->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD;
|
||||
glm::vec3 myPosition = getHead()->getEyePosition();
|
||||
CameraMode mode = qApp->getCamera().getMode();
|
||||
if (mode == CAMERA_MODE_FIRST_PERSON) {
|
||||
myPosition = qApp->getCamera().getPosition();
|
||||
}
|
||||
|
||||
float bestCost = FLT_MAX;
|
||||
std::shared_ptr<Avatar> bestAvatar;
|
||||
|
||||
foreach (const AvatarSharedPointer& avatarData, hash) {
|
||||
std::shared_ptr<Avatar> avatar = std::static_pointer_cast<Avatar>(avatarData);
|
||||
if (!avatar->isMyAvatar() && avatar->isInitialized()) {
|
||||
glm::vec3 otherForward = avatar->getHead()->getForwardDirection();
|
||||
glm::vec3 otherPosition = avatar->getHead()->getEyePosition();
|
||||
const float TIME_WITHOUT_TALKING_THRESHOLD = 1.0f;
|
||||
bool otherIsTalking = avatar->getHead()->getTimeWithoutTalking() <= TIME_WITHOUT_TALKING_THRESHOLD;
|
||||
bool lookingAtOtherAlready = _lookAtTargetAvatar.lock().get() == avatar.get();
|
||||
float cost = lookAtCostFunction(myForward, myPosition, otherForward, otherPosition, otherIsTalking, lookingAtOtherAlready);
|
||||
if (cost < bestCost) {
|
||||
bestCost = cost;
|
||||
bestAvatar = avatar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestAvatar) {
|
||||
_lookAtTargetAvatar = bestAvatar;
|
||||
_targetAvatarPosition = bestAvatar->getWorldPosition();
|
||||
} else {
|
||||
_lookAtTargetAvatar.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void MyAvatar::snapOtherAvatarLookAtTargetsToMe(const AvatarHash& hash) {
|
||||
foreach (const AvatarSharedPointer& avatarData, hash) {
|
||||
std::shared_ptr<Avatar> avatar = std::static_pointer_cast<Avatar>(avatarData);
|
||||
if (!avatar->isMyAvatar() && avatar->isInitialized()) {
|
||||
if (_lookAtSnappingEnabled && avatar->getLookAtSnappingEnabled() && isLookingAtMe(avatar)) {
|
||||
|
||||
// Alter their gaze to look directly at my camera; this looks more natural than looking at my avatar's face.
|
||||
|
@ -1614,10 +2113,19 @@ void MyAvatar::updateLookAtTargetAvatar() {
|
|||
avatar->getHead()->clearCorrectedLookAtPosition();
|
||||
}
|
||||
}
|
||||
auto avatarPointer = _lookAtTargetAvatar.lock();
|
||||
if (avatarPointer) {
|
||||
static_pointer_cast<Avatar>(avatarPointer)->setIsLookAtTarget(true);
|
||||
}
|
||||
}
|
||||
|
||||
void MyAvatar::updateLookAtTargetAvatar() {
|
||||
|
||||
// The AvatarManager is a mutable class shared by many threads. We make a thread-safe deep copy of it,
|
||||
// to avoid having to hold a lock while we iterate over all the avatars within.
|
||||
AvatarHash hash = DependencyManager::get<AvatarManager>()->getHashCopy();
|
||||
|
||||
// determine what the best look at target for my avatar should be.
|
||||
computeMyLookAtTarget(hash);
|
||||
|
||||
// snap look at position for avatars that are looking at me.
|
||||
snapOtherAvatarLookAtTargetsToMe(hash);
|
||||
}
|
||||
|
||||
void MyAvatar::clearLookAtTargetAvatar() {
|
||||
|
@ -1858,8 +2366,11 @@ void MyAvatar::clearAvatarEntities() {
|
|||
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
||||
|
||||
AvatarEntityMap avatarEntities = getAvatarEntityData();
|
||||
for (auto entityID : avatarEntities.keys()) {
|
||||
QList<QUuid> avatarEntityIDs;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
avatarEntityIDs = _packedAvatarEntityData.keys();
|
||||
});
|
||||
for (const auto& entityID : avatarEntityIDs) {
|
||||
entityTree->withWriteLock([&entityID, &entityTree] {
|
||||
// remove this entity first from the entity tree
|
||||
entityTree->deleteEntity(entityID, true, true);
|
||||
|
@ -1874,10 +2385,12 @@ void MyAvatar::clearAvatarEntities() {
|
|||
void MyAvatar::removeWearableAvatarEntities() {
|
||||
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
||||
|
||||
if (entityTree) {
|
||||
AvatarEntityMap avatarEntities = getAvatarEntityData();
|
||||
for (auto entityID : avatarEntities.keys()) {
|
||||
QList<QUuid> avatarEntityIDs;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
avatarEntityIDs = _packedAvatarEntityData.keys();
|
||||
});
|
||||
for (const auto& entityID : avatarEntityIDs) {
|
||||
auto entity = entityTree->findEntityByID(entityID);
|
||||
if (entity && isWearableEntity(entity)) {
|
||||
entityTree->withWriteLock([&entityID, &entityTree] {
|
||||
|
@ -1894,13 +2407,16 @@ void MyAvatar::removeWearableAvatarEntities() {
|
|||
}
|
||||
|
||||
QVariantList MyAvatar::getAvatarEntitiesVariant() {
|
||||
// NOTE: this method is NOT efficient
|
||||
QVariantList avatarEntitiesData;
|
||||
QScriptEngine scriptEngine;
|
||||
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
||||
if (entityTree) {
|
||||
AvatarEntityMap avatarEntities = getAvatarEntityData();
|
||||
for (auto entityID : avatarEntities.keys()) {
|
||||
QList<QUuid> avatarEntityIDs;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
avatarEntityIDs = _packedAvatarEntityData.keys();
|
||||
});
|
||||
for (const auto& entityID : avatarEntityIDs) {
|
||||
auto entity = entityTree->findEntityByID(entityID);
|
||||
if (!entity) {
|
||||
continue;
|
||||
|
@ -1911,7 +2427,7 @@ QVariantList MyAvatar::getAvatarEntitiesVariant() {
|
|||
desiredProperties += PROP_LOCAL_POSITION;
|
||||
desiredProperties += PROP_LOCAL_ROTATION;
|
||||
EntityItemProperties entityProperties = entity->getProperties(desiredProperties);
|
||||
QScriptValue scriptProperties = EntityItemPropertiesToScriptValue(&scriptEngine, entityProperties);
|
||||
QScriptValue scriptProperties = EntityItemPropertiesToScriptValue(_myScriptEngine, entityProperties);
|
||||
avatarEntityData["properties"] = scriptProperties.toVariant();
|
||||
avatarEntitiesData.append(QVariant(avatarEntityData));
|
||||
}
|
||||
|
@ -2300,17 +2816,17 @@ void MyAvatar::setAttachmentData(const QVector<AttachmentData>& attachmentData)
|
|||
}
|
||||
|
||||
QVector<AttachmentData> MyAvatar::getAttachmentData() const {
|
||||
QVector<AttachmentData> avatarData;
|
||||
auto avatarEntities = getAvatarEntityData();
|
||||
AvatarEntityMap::const_iterator dataItr = avatarEntities.begin();
|
||||
while (dataItr != avatarEntities.end()) {
|
||||
QUuid entityID = dataItr.key();
|
||||
QVector<AttachmentData> attachmentData;
|
||||
QList<QUuid> avatarEntityIDs;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
avatarEntityIDs = _packedAvatarEntityData.keys();
|
||||
});
|
||||
for (const auto& entityID : avatarEntityIDs) {
|
||||
auto properties = DependencyManager::get<EntityScriptingInterface>()->getEntityProperties(entityID);
|
||||
AttachmentData data = entityPropertiesToAttachmentData(properties);
|
||||
avatarData.append(data);
|
||||
dataItr++;
|
||||
attachmentData.append(data);
|
||||
}
|
||||
return avatarData;
|
||||
return attachmentData;
|
||||
}
|
||||
|
||||
QVariantList MyAvatar::getAttachmentsVariant() const {
|
||||
|
@ -2339,16 +2855,16 @@ void MyAvatar::setAttachmentsVariant(const QVariantList& variant) {
|
|||
}
|
||||
|
||||
bool MyAvatar::findAvatarEntity(const QString& modelURL, const QString& jointName, QUuid& entityID) {
|
||||
auto avatarEntities = getAvatarEntityData();
|
||||
AvatarEntityMap::const_iterator dataItr = avatarEntities.begin();
|
||||
while (dataItr != avatarEntities.end()) {
|
||||
entityID = dataItr.key();
|
||||
QList<QUuid> avatarEntityIDs;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
avatarEntityIDs = _packedAvatarEntityData.keys();
|
||||
});
|
||||
for (const auto& entityID : avatarEntityIDs) {
|
||||
auto props = DependencyManager::get<EntityScriptingInterface>()->getEntityProperties(entityID);
|
||||
if (props.getModelURL() == modelURL &&
|
||||
(jointName.isEmpty() || props.getParentJointIndex() == getJointIndex(jointName))) {
|
||||
return true;
|
||||
}
|
||||
dataItr++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -2662,9 +3178,10 @@ void MyAvatar::updateOrientation(float deltaTime) {
|
|||
_bodyYawDelta = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
float totalBodyYaw = _bodyYawDelta * deltaTime;
|
||||
|
||||
// Rotate directly proportional to delta yaw and delta pitch from right-click mouse movement.
|
||||
totalBodyYaw += getDriveKey(DELTA_YAW) * _yawSpeed / YAW_SPEED_DEFAULT;
|
||||
|
||||
// Comfort Mode: If you press any of the left/right rotation drive keys or input, you'll
|
||||
// get an instantaneous 15 degree turn. If you keep holding the key down you'll get another
|
||||
|
@ -2732,7 +3249,8 @@ void MyAvatar::updateOrientation(float deltaTime) {
|
|||
head->setBaseRoll(ROLL(euler));
|
||||
} else {
|
||||
head->setBaseYaw(0.0f);
|
||||
head->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime);
|
||||
head->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime
|
||||
+ getDriveKey(DELTA_PITCH) * _pitchSpeed / PITCH_SPEED_DEFAULT);
|
||||
head->setBaseRoll(0.0f);
|
||||
}
|
||||
}
|
||||
|
@ -3326,7 +3844,6 @@ void MyAvatar::setCollisionsEnabled(bool enabled) {
|
|||
QMetaObject::invokeMethod(this, "setCollisionsEnabled", Q_ARG(bool, enabled));
|
||||
return;
|
||||
}
|
||||
|
||||
_characterController.setCollisionless(!enabled);
|
||||
emit collisionsEnabledChanged(enabled);
|
||||
}
|
||||
|
@ -3337,6 +3854,20 @@ bool MyAvatar::getCollisionsEnabled() {
|
|||
return _characterController.computeCollisionGroup() != BULLET_COLLISION_GROUP_COLLISIONLESS;
|
||||
}
|
||||
|
||||
void MyAvatar::setOtherAvatarsCollisionsEnabled(bool enabled) {
|
||||
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "setOtherAvatarsCollisionsEnabled", Q_ARG(bool, enabled));
|
||||
return;
|
||||
}
|
||||
_collideWithOtherAvatars = enabled;
|
||||
emit otherAvatarsCollisionsEnabledChanged(enabled);
|
||||
}
|
||||
|
||||
bool MyAvatar::getOtherAvatarsCollisionsEnabled() {
|
||||
return _collideWithOtherAvatars;
|
||||
}
|
||||
|
||||
void MyAvatar::updateCollisionCapsuleCache() {
|
||||
glm::vec3 start, end;
|
||||
float radius;
|
||||
|
|
|
@ -225,6 +225,7 @@ class MyAvatar : public Avatar {
|
|||
Q_PROPERTY(bool centerOfGravityModelEnabled READ getCenterOfGravityModelEnabled WRITE setCenterOfGravityModelEnabled)
|
||||
Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled)
|
||||
Q_PROPERTY(bool collisionsEnabled READ getCollisionsEnabled WRITE setCollisionsEnabled)
|
||||
Q_PROPERTY(bool otherAvatarsCollisionsEnabled READ getOtherAvatarsCollisionsEnabled WRITE setOtherAvatarsCollisionsEnabled)
|
||||
Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled)
|
||||
Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls)
|
||||
Q_PROPERTY(bool showPlayArea READ getShowPlayArea WRITE setShowPlayArea)
|
||||
|
@ -264,6 +265,8 @@ public:
|
|||
STEP_YAW,
|
||||
PITCH,
|
||||
ZOOM,
|
||||
DELTA_YAW,
|
||||
DELTA_PITCH,
|
||||
MAX_DRIVE_KEYS
|
||||
};
|
||||
Q_ENUM(DriveKeys)
|
||||
|
@ -574,9 +577,11 @@ public:
|
|||
float getHMDRollControlRate() const { return _hmdRollControlRate; }
|
||||
|
||||
// get/set avatar data
|
||||
void resizeAvatarEntitySettingHandles(unsigned int avatarEntityIndex);
|
||||
void resizeAvatarEntitySettingHandles(uint32_t maxIndex);
|
||||
void saveData();
|
||||
void saveAvatarEntityDataToSettings();
|
||||
void loadData();
|
||||
void loadAvatarEntityDataFromSettings();
|
||||
|
||||
void saveAttachmentData(const AttachmentData& attachment) const;
|
||||
AttachmentData loadAttachmentData(const QUrl& modelURL, const QString& jointName = QString()) const;
|
||||
|
@ -832,6 +837,8 @@ public:
|
|||
|
||||
AvatarWeakPointer getLookAtTargetAvatar() const { return _lookAtTargetAvatar; }
|
||||
void updateLookAtTargetAvatar();
|
||||
void computeMyLookAtTarget(const AvatarHash& hash);
|
||||
void snapOtherAvatarLookAtTargetsToMe(const AvatarHash& hash);
|
||||
void clearLookAtTargetAvatar();
|
||||
|
||||
virtual void setJointRotations(const QVector<glm::quat>& jointRotations) override;
|
||||
|
@ -1062,6 +1069,18 @@ public:
|
|||
*/
|
||||
Q_INVOKABLE bool getCollisionsEnabled();
|
||||
|
||||
/**jsdoc
|
||||
* @function MyAvatar.setOtherAvatarsCollisionsEnabled
|
||||
* @param {boolean} enabled
|
||||
*/
|
||||
Q_INVOKABLE void setOtherAvatarsCollisionsEnabled(bool enabled);
|
||||
|
||||
/**jsdoc
|
||||
* @function MyAvatar.getOtherAvatarsCollisionsEnabled
|
||||
* @returns {boolean}
|
||||
*/
|
||||
Q_INVOKABLE bool getOtherAvatarsCollisionsEnabled();
|
||||
|
||||
/**jsdoc
|
||||
* @function MyAvatar.getCollisionCapsule
|
||||
* @returns {object}
|
||||
|
@ -1169,6 +1188,7 @@ public:
|
|||
virtual void setAttachmentsVariant(const QVariantList& variant) override;
|
||||
|
||||
glm::vec3 getNextPosition() { return _goToPending ? _goToPosition : getWorldPosition(); }
|
||||
void prepareAvatarEntityDataForReload();
|
||||
|
||||
/**jsdoc
|
||||
* Create a new grab.
|
||||
|
@ -1189,6 +1209,11 @@ public:
|
|||
*/
|
||||
Q_INVOKABLE void releaseGrab(const QUuid& grabID);
|
||||
|
||||
AvatarEntityMap getAvatarEntityData() const override;
|
||||
void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override;
|
||||
void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) override;
|
||||
void avatarEntityDataToJson(QJsonObject& root) const override;
|
||||
|
||||
public slots:
|
||||
|
||||
/**jsdoc
|
||||
|
@ -1387,6 +1412,10 @@ public slots:
|
|||
*/
|
||||
bool getEnableMeshVisible() const override;
|
||||
|
||||
void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) override;
|
||||
void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true) override;
|
||||
void sanitizeAvatarEntityProperties(EntityItemProperties& properties) const;
|
||||
|
||||
/**jsdoc
|
||||
* Set whether or not your avatar mesh is visible.
|
||||
* @function MyAvatar.setEnableMeshVisible
|
||||
|
@ -1489,6 +1518,14 @@ signals:
|
|||
*/
|
||||
void collisionsEnabledChanged(bool enabled);
|
||||
|
||||
/**jsdoc
|
||||
* Triggered when collisions with other avatars enabled or disabled
|
||||
* @function MyAvatar.otherAvatarsCollisionsEnabledChanged
|
||||
* @param {boolean} enabled
|
||||
* @returns {Signal}
|
||||
*/
|
||||
void otherAvatarsCollisionsEnabledChanged(bool enabled);
|
||||
|
||||
/**jsdoc
|
||||
* Triggered when avatar's animation url changes
|
||||
* @function MyAvatar.animGraphUrlChanged
|
||||
|
@ -1578,23 +1615,24 @@ signals:
|
|||
*/
|
||||
void disableHandTouchForIDChanged(const QUuid& entityID, bool disable);
|
||||
|
||||
|
||||
private slots:
|
||||
void leaveDomain();
|
||||
void updateCollisionCapsuleCache();
|
||||
|
||||
protected:
|
||||
void handleChangedAvatarEntityData();
|
||||
virtual void beParentOfChild(SpatiallyNestablePointer newChild) const override;
|
||||
virtual void forgetChild(SpatiallyNestablePointer newChild) const override;
|
||||
virtual void recalculateChildCauterization() const override;
|
||||
|
||||
private:
|
||||
bool updateStaleAvatarEntityBlobs() const;
|
||||
|
||||
bool requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& positionOut);
|
||||
|
||||
virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) override;
|
||||
|
||||
void simulate(float deltaTime);
|
||||
void simulate(float deltaTime, bool inView) override;
|
||||
void updateFromTrackers(float deltaTime);
|
||||
void saveAvatarUrl();
|
||||
virtual void render(RenderArgs* renderArgs) override;
|
||||
|
@ -1897,6 +1935,7 @@ private:
|
|||
bool _haveReceivedHeightLimitsFromDomain { false };
|
||||
int _disableHandTouchCount { 0 };
|
||||
bool _skeletonModelLoaded { false };
|
||||
bool _reloadAvatarEntityDataFromSettings { true };
|
||||
|
||||
Setting::Handle<QString> _dominantHandSetting;
|
||||
Setting::Handle<float> _headPitchSetting;
|
||||
|
@ -1915,6 +1954,38 @@ private:
|
|||
Setting::Handle<bool> _allowTeleportingSetting { "allowTeleporting", true };
|
||||
std::vector<Setting::Handle<QUuid>> _avatarEntityIDSettings;
|
||||
std::vector<Setting::Handle<QByteArray>> _avatarEntityDataSettings;
|
||||
|
||||
// AvatarEntities stuff:
|
||||
// We cache the "map of unfortunately-formatted-binary-blobs" because they are expensive to compute
|
||||
// Do not confuse these with AvatarData::_packedAvatarEntityData which are in wire-format.
|
||||
mutable AvatarEntityMap _cachedAvatarEntityBlobs;
|
||||
|
||||
// We collect changes to AvatarEntities and then handle them all in one spot per frame: updateAvatarEntities().
|
||||
// Basically this is a "transaction pattern" with an extra complication: these changes can come from two
|
||||
// "directions" and the "authoritative source" of each direction is different, so maintain two distinct sets of
|
||||
// transaction lists;
|
||||
//
|
||||
// The _entitiesToDelete/Add/Update lists are for changes whose "authoritative sources" are already
|
||||
// correctly stored in _cachedAvatarEntityBlobs. These come from loadAvatarEntityDataFromSettings() and
|
||||
// setAvatarEntityData(). These changes need to be extracted from _cachedAvatarEntityBlobs and applied to
|
||||
// real EntityItems.
|
||||
std::vector<QUuid> _entitiesToDelete;
|
||||
std::vector<QUuid> _entitiesToAdd;
|
||||
std::vector<QUuid> _entitiesToUpdate;
|
||||
//
|
||||
// The _cachedAvatarEntityBlobsToDelete/Add/Update lists are for changes whose "authoritative sources" are
|
||||
// already reflected in real EntityItems. These changes need to be propagated to _cachedAvatarEntityBlobs
|
||||
// and eventually to settings.
|
||||
std::vector<QUuid> _cachedAvatarEntityBlobsToDelete;
|
||||
std::vector<QUuid> _cachedAvatarEntityBlobsToAddOrUpdate;
|
||||
std::vector<QUuid> _cachedAvatarEntityBlobUpdatesToSkip;
|
||||
//
|
||||
// Also these lists for tracking delayed changes to blobs and Settings
|
||||
mutable std::set<QUuid> _staleCachedAvatarEntityBlobs;
|
||||
//
|
||||
// keep a ScriptEngine around so we don't have to instantiate on the fly (these are very slow to create/delete)
|
||||
QScriptEngine* _myScriptEngine { nullptr };
|
||||
bool _needToSaveAvatarEntitySettings { false };
|
||||
};
|
||||
|
||||
QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode);
|
||||
|
|
|
@ -7,10 +7,18 @@
|
|||
//
|
||||
|
||||
#include "OtherAvatar.h"
|
||||
#include "Application.h"
|
||||
|
||||
#include <glm/gtx/norm.hpp>
|
||||
#include <glm/gtx/vector_angle.hpp>
|
||||
|
||||
#include <AvatarLogging.h>
|
||||
|
||||
#include "Application.h"
|
||||
#include "AvatarMotionState.h"
|
||||
|
||||
const float DISPLAYNAME_FADE_TIME = 0.5f;
|
||||
const float DISPLAYNAME_FADE_FACTOR = pow(0.01f, 1.0f / DISPLAYNAME_FADE_TIME);
|
||||
|
||||
static glm::u8vec3 getLoadingOrbColor(Avatar::LoadingStatus loadingStatus) {
|
||||
|
||||
const glm::u8vec3 NO_MODEL_COLOR(0xe3, 0xe3, 0xe3);
|
||||
|
@ -120,7 +128,7 @@ bool OtherAvatar::shouldBeInPhysicsSimulation() const {
|
|||
}
|
||||
|
||||
bool OtherAvatar::needsPhysicsUpdate() const {
|
||||
constexpr uint32_t FLAGS_OF_INTEREST = Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS | Simulation::DIRTY_POSITION;
|
||||
constexpr uint32_t FLAGS_OF_INTEREST = Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS | Simulation::DIRTY_POSITION | Simulation::DIRTY_COLLISION_GROUP;
|
||||
return (_motionState && (bool)(_motionState->getIncomingDirtyFlags() & FLAGS_OF_INTEREST));
|
||||
}
|
||||
|
||||
|
@ -129,3 +137,306 @@ void OtherAvatar::rebuildCollisionShape() {
|
|||
_motionState->addDirtyFlags(Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS);
|
||||
}
|
||||
}
|
||||
|
||||
void OtherAvatar::updateCollisionGroup(bool myAvatarCollide) {
|
||||
if (_motionState) {
|
||||
bool collides = _motionState->getCollisionGroup() == BULLET_COLLISION_GROUP_OTHER_AVATAR && myAvatarCollide;
|
||||
if (_collideWithOtherAvatars != collides) {
|
||||
if (!myAvatarCollide) {
|
||||
_collideWithOtherAvatars = false;
|
||||
}
|
||||
auto newCollisionGroup = _collideWithOtherAvatars ? BULLET_COLLISION_GROUP_OTHER_AVATAR : BULLET_COLLISION_GROUP_COLLISIONLESS;
|
||||
_motionState->setCollisionGroup(newCollisionGroup);
|
||||
_motionState->addDirtyFlags(Simulation::DIRTY_COLLISION_GROUP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OtherAvatar::simulate(float deltaTime, bool inView) {
|
||||
PROFILE_RANGE(simulation, "simulate");
|
||||
|
||||
_globalPosition = _transit.isActive() ? _transit.getCurrentPosition() : _serverPosition;
|
||||
if (!hasParent()) {
|
||||
setLocalPosition(_globalPosition);
|
||||
}
|
||||
|
||||
_simulationRate.increment();
|
||||
if (inView) {
|
||||
_simulationInViewRate.increment();
|
||||
}
|
||||
|
||||
PerformanceTimer perfTimer("simulate");
|
||||
{
|
||||
PROFILE_RANGE(simulation, "updateJoints");
|
||||
if (inView) {
|
||||
Head* head = getHead();
|
||||
if (_hasNewJointData || _transit.isActive()) {
|
||||
_skeletonModel->getRig().copyJointsFromJointData(_jointData);
|
||||
glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset());
|
||||
_skeletonModel->getRig().computeExternalPoses(rootTransform);
|
||||
_jointDataSimulationRate.increment();
|
||||
|
||||
_skeletonModel->simulate(deltaTime, true);
|
||||
|
||||
locationChanged(); // joints changed, so if there are any children, update them.
|
||||
_hasNewJointData = false;
|
||||
|
||||
glm::vec3 headPosition = getWorldPosition();
|
||||
if (!_skeletonModel->getHeadPosition(headPosition)) {
|
||||
headPosition = getWorldPosition();
|
||||
}
|
||||
head->setPosition(headPosition);
|
||||
}
|
||||
head->setScale(getModelScale());
|
||||
head->simulate(deltaTime);
|
||||
relayJointDataToChildren();
|
||||
} else {
|
||||
// a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated.
|
||||
_skeletonModel->simulate(deltaTime, false);
|
||||
}
|
||||
_skeletonModelSimulationRate.increment();
|
||||
}
|
||||
|
||||
// update animation for display name fade in/out
|
||||
if ( _displayNameTargetAlpha != _displayNameAlpha) {
|
||||
// the alpha function is
|
||||
// Fade out => alpha(t) = factor ^ t => alpha(t+dt) = alpha(t) * factor^(dt)
|
||||
// Fade in => alpha(t) = 1 - factor^t => alpha(t+dt) = 1-(1-alpha(t))*coef^(dt)
|
||||
// factor^(dt) = coef
|
||||
float coef = pow(DISPLAYNAME_FADE_FACTOR, deltaTime);
|
||||
if (_displayNameTargetAlpha < _displayNameAlpha) {
|
||||
// Fading out
|
||||
_displayNameAlpha *= coef;
|
||||
} else {
|
||||
// Fading in
|
||||
_displayNameAlpha = 1.0f - (1.0f - _displayNameAlpha) * coef;
|
||||
}
|
||||
_displayNameAlpha = glm::abs(_displayNameAlpha - _displayNameTargetAlpha) < 0.01f ? _displayNameTargetAlpha : _displayNameAlpha;
|
||||
}
|
||||
|
||||
{
|
||||
PROFILE_RANGE(simulation, "misc");
|
||||
measureMotionDerivatives(deltaTime);
|
||||
simulateAttachments(deltaTime);
|
||||
updatePalms();
|
||||
}
|
||||
{
|
||||
PROFILE_RANGE(simulation, "entities");
|
||||
handleChangedAvatarEntityData();
|
||||
updateAttachedAvatarEntities();
|
||||
}
|
||||
|
||||
{
|
||||
PROFILE_RANGE(simulation, "grabs");
|
||||
updateGrabs();
|
||||
}
|
||||
|
||||
updateFadingStatus();
|
||||
}
|
||||
|
||||
void OtherAvatar::handleChangedAvatarEntityData() {
|
||||
PerformanceTimer perfTimer("attachments");
|
||||
|
||||
// AVATAR ENTITY UPDATE FLOW
|
||||
// - if queueEditEntityMessage() sees "AvatarEntity" HostType it calls _myAvatar->storeAvatarEntityDataPayload()
|
||||
// - storeAvatarEntityDataPayload() saves the payload and flags the trait instance for the entity as updated,
|
||||
// - ClientTraitsHandler::sendChangedTraitsToMixea() sends the entity bytes to the mixer which relays them to other interfaces
|
||||
// - AvatarHashMap::processBulkAvatarTraits() on other interfaces calls avatar->processTraitInstance()
|
||||
// - AvatarData::processTraitInstance() calls storeAvatarEntityDataPayload(), which sets _avatarEntityDataChanged = true
|
||||
// - (My)Avatar::simulate() calls handleChangedAvatarEntityData() every frame which checks _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
|
||||
// - (My)Avatar::simulate() calls handleChangedAvatarEntityData() every frame which checks _avatarEntityDataChanged
|
||||
// and here we are...
|
||||
|
||||
if (!_avatarEntityDataChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
||||
if (!entityTree) {
|
||||
return;
|
||||
}
|
||||
|
||||
PackedAvatarEntityMap packedAvatarEntityData;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
packedAvatarEntityData = _packedAvatarEntityData;
|
||||
});
|
||||
entityTree->withWriteLock([&] {
|
||||
AvatarEntityMap::const_iterator dataItr = packedAvatarEntityData.begin();
|
||||
while (dataItr != packedAvatarEntityData.end()) {
|
||||
// compute hash of data. TODO? cache this?
|
||||
QByteArray data = dataItr.value();
|
||||
uint32_t newHash = qHash(data);
|
||||
|
||||
// check to see if we recognize this hash and whether it was already successfully processed
|
||||
QUuid entityID = dataItr.key();
|
||||
MapOfAvatarEntityDataHashes::iterator stateItr = _avatarEntityDataHashes.find(entityID);
|
||||
if (stateItr != _avatarEntityDataHashes.end()) {
|
||||
if (stateItr.value().success) {
|
||||
if (newHash == stateItr.value().hash) {
|
||||
// data hasn't changed --> nothing to do
|
||||
++dataItr;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// NOTE: if the data was unsuccessful in producing an entity in the past
|
||||
// we will try again just in case something changed (unlikely).
|
||||
// Unfortunately constantly trying to build the entity for this data costs
|
||||
// CPU cycles that we'd rather not spend.
|
||||
// TODO? put a maximum number of tries on this?
|
||||
}
|
||||
} else {
|
||||
// sanity check data
|
||||
QUuid id;
|
||||
EntityTypes::EntityType type;
|
||||
EntityTypes::extractEntityTypeAndID((unsigned char*)(data.data()), data.size(), type, id);
|
||||
if (id != entityID || !EntityTypes::typeIsValid(type)) {
|
||||
// skip for corrupt
|
||||
++dataItr;
|
||||
continue;
|
||||
}
|
||||
// remember this hash for the future
|
||||
stateItr = _avatarEntityDataHashes.insert(entityID, AvatarEntityDataHash(newHash));
|
||||
}
|
||||
++dataItr;
|
||||
|
||||
EntityItemProperties properties;
|
||||
int32_t bytesLeftToRead = data.size();
|
||||
unsigned char* dataAt = (unsigned char*)(data.data());
|
||||
if (!properties.constructFromBuffer(dataAt, bytesLeftToRead)) {
|
||||
// properties are corrupt
|
||||
continue;
|
||||
}
|
||||
|
||||
properties.setEntityHostType(entity::HostType::AVATAR);
|
||||
properties.setOwningAvatarID(getID());
|
||||
|
||||
// there's no entity-server to tell us we're the simulation owner, so always set the
|
||||
// simulationOwner to the owningAvatarID and a high priority.
|
||||
properties.setSimulationOwner(getID(), AVATAR_ENTITY_SIMULATION_PRIORITY);
|
||||
|
||||
if (properties.getParentID() == AVATAR_SELF_ID) {
|
||||
properties.setParentID(getID());
|
||||
}
|
||||
|
||||
// NOTE: if this avatar entity is not attached to us, strip its entity script completely...
|
||||
auto attachedScript = properties.getScript();
|
||||
if (!isMyAvatar() && !attachedScript.isEmpty()) {
|
||||
QString noScript;
|
||||
properties.setScript(noScript);
|
||||
}
|
||||
|
||||
auto specifiedHref = properties.getHref();
|
||||
if (!isMyAvatar() && !specifiedHref.isEmpty()) {
|
||||
qCDebug(avatars) << "removing entity href from avatar attached entity:" << entityID << "old href:" << specifiedHref;
|
||||
QString noHref;
|
||||
properties.setHref(noHref);
|
||||
}
|
||||
|
||||
// When grabbing avatar entities, they are parented to the joint moving them, then when un-grabbed
|
||||
// they go back to the default parent (null uuid). When un-gripped, others saw the entity disappear.
|
||||
// The thinking here is the local position was noticed as changing, but not the parentID (since it is now
|
||||
// back to the default), and the entity flew off somewhere. Marking all changed definitely fixes this,
|
||||
// and seems safe (per Seth).
|
||||
properties.markAllChanged();
|
||||
|
||||
// try to build the entity
|
||||
EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID));
|
||||
bool success = true;
|
||||
if (entity) {
|
||||
QUuid oldParentID = entity->getParentID();
|
||||
if (entityTree->updateEntity(entityID, properties)) {
|
||||
entity->updateLastEditedFromRemote();
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
if (oldParentID != entity->getParentID()) {
|
||||
if (entity->getParentID() == getID()) {
|
||||
onAddAttachedAvatarEntity(entityID);
|
||||
} else if (oldParentID == getID()) {
|
||||
onRemoveAttachedAvatarEntity(entityID);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
entity = entityTree->addEntity(entityID, properties);
|
||||
if (!entity) {
|
||||
success = false;
|
||||
} else if (entity->getParentID() == getID()) {
|
||||
onAddAttachedAvatarEntity(entityID);
|
||||
}
|
||||
}
|
||||
stateItr.value().success = success;
|
||||
}
|
||||
|
||||
AvatarEntityIDs recentlyRemovedAvatarEntities = getAndClearRecentlyRemovedIDs();
|
||||
if (!recentlyRemovedAvatarEntities.empty()) {
|
||||
// only lock this thread when absolutely necessary
|
||||
AvatarEntityMap packedAvatarEntityData;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
packedAvatarEntityData = _packedAvatarEntityData;
|
||||
});
|
||||
foreach (auto entityID, recentlyRemovedAvatarEntities) {
|
||||
if (!packedAvatarEntityData.contains(entityID)) {
|
||||
entityTree->deleteEntity(entityID, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move this outside of tree lock
|
||||
// remove stale data hashes
|
||||
foreach (auto entityID, recentlyRemovedAvatarEntities) {
|
||||
MapOfAvatarEntityDataHashes::iterator stateItr = _avatarEntityDataHashes.find(entityID);
|
||||
if (stateItr != _avatarEntityDataHashes.end()) {
|
||||
_avatarEntityDataHashes.erase(stateItr);
|
||||
}
|
||||
onRemoveAttachedAvatarEntity(entityID);
|
||||
}
|
||||
}
|
||||
if (packedAvatarEntityData.size() != _avatarEntityForRecording.size()) {
|
||||
createRecordingIDs();
|
||||
}
|
||||
});
|
||||
|
||||
setAvatarEntityDataChanged(false);
|
||||
}
|
||||
|
||||
void OtherAvatar::onAddAttachedAvatarEntity(const QUuid& id) {
|
||||
for (uint32_t i = 0; i < _attachedAvatarEntities.size(); ++i) {
|
||||
if (_attachedAvatarEntities[i] == id) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_attachedAvatarEntities.push_back(id);
|
||||
}
|
||||
|
||||
void OtherAvatar::onRemoveAttachedAvatarEntity(const QUuid& id) {
|
||||
for (uint32_t i = 0; i < _attachedAvatarEntities.size(); ++i) {
|
||||
if (_attachedAvatarEntities[i] == id) {
|
||||
if (i != _attachedAvatarEntities.size() - 1) {
|
||||
_attachedAvatarEntities[i] = _attachedAvatarEntities.back();
|
||||
}
|
||||
_attachedAvatarEntities.pop_back();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OtherAvatar::updateAttachedAvatarEntities() {
|
||||
if (!_attachedAvatarEntities.empty()) {
|
||||
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||
if (!treeRenderer) {
|
||||
return;
|
||||
}
|
||||
for (const QUuid& id : _attachedAvatarEntities) {
|
||||
treeRenderer->onEntityChanged(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#define hifi_OtherAvatar_h
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <avatars-renderer/Avatar.h>
|
||||
#include <workload/Space.h>
|
||||
|
@ -45,9 +46,19 @@ public:
|
|||
bool shouldBeInPhysicsSimulation() const;
|
||||
bool needsPhysicsUpdate() const;
|
||||
|
||||
void updateCollisionGroup(bool myAvatarCollide);
|
||||
|
||||
void simulate(float deltaTime, bool inView) override;
|
||||
|
||||
friend AvatarManager;
|
||||
|
||||
protected:
|
||||
void handleChangedAvatarEntityData();
|
||||
void updateAttachedAvatarEntities();
|
||||
void onAddAttachedAvatarEntity(const QUuid& id);
|
||||
void onRemoveAttachedAvatarEntity(const QUuid& id);
|
||||
|
||||
std::vector<QUuid> _attachedAvatarEntities;
|
||||
std::shared_ptr<Sphere3DOverlay> _otherAvatarOrbMeshPlaceholder { nullptr };
|
||||
OverlayID _otherAvatarOrbMeshPlaceholderID { UNKNOWN_OVERLAY_ID };
|
||||
AvatarMotionState* _motionState { nullptr };
|
||||
|
|
|
@ -223,7 +223,7 @@ Pointer::Buttons PathPointer::getPressedButtons(const PickResultPointer& pickRes
|
|||
std::string button = trigger.getButton();
|
||||
TriggerState& state = _states[button];
|
||||
// TODO: right now, LaserPointers don't support axes, only on/off buttons
|
||||
if (trigger.getEndpoint()->peek() >= 1.0f) {
|
||||
if (trigger.getEndpoint()->peek().value >= 1.0f) {
|
||||
toReturn.insert(button);
|
||||
|
||||
if (_previousButtons.find(button) == _previousButtons.end()) {
|
||||
|
|
|
@ -25,12 +25,12 @@ float ClipboardScriptingInterface::getClipboardContentsLargestDimension() {
|
|||
return qApp->getEntityClipboard()->getContentsLargestDimension();
|
||||
}
|
||||
|
||||
bool ClipboardScriptingInterface::exportEntities(const QString& filename, const QVector<EntityItemID>& entityIDs) {
|
||||
bool ClipboardScriptingInterface::exportEntities(const QString& filename, const QVector<QUuid>& entityIDs) {
|
||||
bool retVal;
|
||||
BLOCKING_INVOKE_METHOD(qApp, "exportEntities",
|
||||
Q_RETURN_ARG(bool, retVal),
|
||||
Q_ARG(const QString&, filename),
|
||||
Q_ARG(const QVector<EntityItemID>&, entityIDs));
|
||||
Q_ARG(const QVector<QUuid>&, entityIDs));
|
||||
return retVal;
|
||||
}
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ public:
|
|||
* @param {Uuid[]} entityIDs Array of IDs of the entities to export.
|
||||
* @returns {boolean} <code>true</code> if the export was successful, otherwise <code>false</code>.
|
||||
*/
|
||||
Q_INVOKABLE bool exportEntities(const QString& filename, const QVector<EntityItemID>& entityIDs);
|
||||
Q_INVOKABLE bool exportEntities(const QString& filename, const QVector<QUuid>& entityIDs);
|
||||
|
||||
/**jsdoc
|
||||
* Export the entities with centers within a cube to a JSON file.
|
||||
|
|
|
@ -104,7 +104,7 @@ class ScriptEngine;
|
|||
* <ul>
|
||||
* <li>{@link Controller.getValue|getValue}</li>
|
||||
* <li>{@link Controller.getAxisValue|getAxisValue}</li>
|
||||
* <li>{@link Controller.getPoseValue|getgetPoseValue}</li>
|
||||
* <li>{@link Controller.getPoseValue|getPoseValue}</li>
|
||||
* <li>{@link Controller.getActionValue|getActionValue}</li>
|
||||
* </ul>
|
||||
*
|
||||
|
|
|
@ -43,7 +43,7 @@ bool MenuScriptingInterface::menuExists(const QString& menu) {
|
|||
if (QThread::currentThread() == qApp->thread()) {
|
||||
return Menu::getInstance()->menuExists(menu);
|
||||
}
|
||||
bool result;
|
||||
bool result { false };
|
||||
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "menuExists",
|
||||
Q_RETURN_ARG(bool, result),
|
||||
Q_ARG(const QString&, menu));
|
||||
|
@ -86,7 +86,7 @@ bool MenuScriptingInterface::menuItemExists(const QString& menu, const QString&
|
|||
if (QThread::currentThread() == qApp->thread()) {
|
||||
return Menu::getInstance()->menuItemExists(menu, menuitem);
|
||||
}
|
||||
bool result;
|
||||
bool result { false };
|
||||
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "menuItemExists",
|
||||
Q_RETURN_ARG(bool, result),
|
||||
Q_ARG(const QString&, menu),
|
||||
|
@ -98,7 +98,7 @@ bool MenuScriptingInterface::isOptionChecked(const QString& menuOption) {
|
|||
if (QThread::currentThread() == qApp->thread()) {
|
||||
return Menu::getInstance()->isOptionChecked(menuOption);
|
||||
}
|
||||
bool result;
|
||||
bool result { false };
|
||||
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "isOptionChecked",
|
||||
Q_RETURN_ARG(bool, result),
|
||||
Q_ARG(const QString&, menuOption));
|
||||
|
@ -115,7 +115,7 @@ bool MenuScriptingInterface::isMenuEnabled(const QString& menuOption) {
|
|||
if (QThread::currentThread() == qApp->thread()) {
|
||||
return Menu::getInstance()->isOptionChecked(menuOption);
|
||||
}
|
||||
bool result;
|
||||
bool result { false };
|
||||
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "isMenuEnabled",
|
||||
Q_RETURN_ARG(bool, result),
|
||||
Q_ARG(const QString&, menuOption));
|
||||
|
|
135
interface/src/scripting/PlatformInfoScriptingInterface.cpp
Normal file
135
interface/src/scripting/PlatformInfoScriptingInterface.cpp
Normal file
|
@ -0,0 +1,135 @@
|
|||
//
|
||||
// Created by Nissim Hadar on 2018/12/28
|
||||
// Copyright 2013-2016 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
#include "PlatformInfoScriptingInterface.h"
|
||||
#include "Application.h"
|
||||
|
||||
#include <thread>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include <Windows.h>
|
||||
#elif defined Q_OS_MAC
|
||||
#include <sstream>
|
||||
#endif
|
||||
|
||||
PlatformInfoScriptingInterface* PlatformInfoScriptingInterface::getInstance() {
|
||||
static PlatformInfoScriptingInterface sharedInstance;
|
||||
return &sharedInstance;
|
||||
}
|
||||
|
||||
QString PlatformInfoScriptingInterface::getOperatingSystemType() {
|
||||
#ifdef Q_OS_WIN
|
||||
return "WINDOWS";
|
||||
#elif defined Q_OS_MAC
|
||||
return "MACOS";
|
||||
#else
|
||||
return "UNKNOWN";
|
||||
#endif
|
||||
}
|
||||
|
||||
QString PlatformInfoScriptingInterface::getCPUBrand() {
|
||||
#ifdef Q_OS_WIN
|
||||
int CPUInfo[4] = { -1 };
|
||||
unsigned nExIds, i = 0;
|
||||
char CPUBrandString[0x40];
|
||||
// Get the information associated with each extended ID.
|
||||
__cpuid(CPUInfo, 0x80000000);
|
||||
nExIds = CPUInfo[0];
|
||||
|
||||
for (i = 0x80000000; i <= nExIds; ++i) {
|
||||
__cpuid(CPUInfo, i);
|
||||
// Interpret CPU brand string
|
||||
if (i == 0x80000002) {
|
||||
memcpy(CPUBrandString, CPUInfo, sizeof(CPUInfo));
|
||||
} else if (i == 0x80000003) {
|
||||
memcpy(CPUBrandString + 16, CPUInfo, sizeof(CPUInfo));
|
||||
} else if (i == 0x80000004) {
|
||||
memcpy(CPUBrandString + 32, CPUInfo, sizeof(CPUInfo));
|
||||
}
|
||||
}
|
||||
|
||||
return CPUBrandString;
|
||||
#elif defined Q_OS_MAC
|
||||
FILE* stream = popen("sysctl -n machdep.cpu.brand_string", "r");
|
||||
|
||||
std::ostringstream hostStream;
|
||||
while (!feof(stream) && !ferror(stream)) {
|
||||
char buf[128];
|
||||
int bytesRead = fread(buf, 1, 128, stream);
|
||||
hostStream.write(buf, bytesRead);
|
||||
}
|
||||
|
||||
return QString::fromStdString(hostStream.str());
|
||||
#else
|
||||
return QString("NO IMPLEMENTED");
|
||||
#endif
|
||||
}
|
||||
|
||||
unsigned int PlatformInfoScriptingInterface::getNumLogicalCores() {
|
||||
|
||||
return std::thread::hardware_concurrency();
|
||||
}
|
||||
|
||||
int PlatformInfoScriptingInterface::getTotalSystemMemoryMB() {
|
||||
#ifdef Q_OS_WIN
|
||||
MEMORYSTATUSEX statex;
|
||||
statex.dwLength = sizeof (statex);
|
||||
GlobalMemoryStatusEx(&statex);
|
||||
return statex.ullTotalPhys / 1024 / 1024;
|
||||
#elif defined Q_OS_MAC
|
||||
FILE* stream = popen("sysctl -a | grep hw.memsize", "r");
|
||||
|
||||
std::ostringstream hostStream;
|
||||
while (!feof(stream) && !ferror(stream)) {
|
||||
char buf[128];
|
||||
int bytesRead = fread(buf, 1, 128, stream);
|
||||
hostStream.write(buf, bytesRead);
|
||||
}
|
||||
|
||||
QString result = QString::fromStdString(hostStream.str());
|
||||
QStringList parts = result.split(' ');
|
||||
return (int)(parts[1].toDouble() / 1024 / 1024);
|
||||
#else
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
QString PlatformInfoScriptingInterface::getGraphicsCardType() {
|
||||
#ifdef Q_OS_WIN
|
||||
return qApp->getGraphicsCardType();
|
||||
#elif defined Q_OS_MAC
|
||||
FILE* stream = popen("system_profiler SPDisplaysDataType | grep Chipset", "r");
|
||||
|
||||
std::ostringstream hostStream;
|
||||
while (!feof(stream) && !ferror(stream)) {
|
||||
char buf[128];
|
||||
int bytesRead = fread(buf, 1, 128, stream);
|
||||
hostStream.write(buf, bytesRead);
|
||||
}
|
||||
|
||||
QString result = QString::fromStdString(hostStream.str());
|
||||
QStringList parts = result.split('\n');
|
||||
for (int i = 0; i < parts.size(); ++i) {
|
||||
if (parts[i].toLower().contains("radeon") || parts[i].toLower().contains("nvidia")) {
|
||||
return parts[i];
|
||||
}
|
||||
}
|
||||
|
||||
// unkown graphics card
|
||||
return "UNKNOWN";
|
||||
#else
|
||||
return QString("NO IMPLEMENTED");
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PlatformInfoScriptingInterface::hasRiftControllers() {
|
||||
return qApp->hasRiftControllers();
|
||||
}
|
||||
|
||||
bool PlatformInfoScriptingInterface::hasViveControllers() {
|
||||
return qApp->hasViveControllers();
|
||||
}
|
70
interface/src/scripting/PlatformInfoScriptingInterface.h
Normal file
70
interface/src/scripting/PlatformInfoScriptingInterface.h
Normal file
|
@ -0,0 +1,70 @@
|
|||
//
|
||||
// Created by Nissim Hadar on 2018/12/28
|
||||
// Copyright 2013-2016 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#ifndef hifi_PlatformInfoScriptingInterface_h
|
||||
#define hifi_PlatformInfoScriptingInterface_h
|
||||
|
||||
#include <QtCore/QObject>
|
||||
|
||||
class QScriptValue;
|
||||
|
||||
class PlatformInfoScriptingInterface : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public slots:
|
||||
static PlatformInfoScriptingInterface* getInstance();
|
||||
|
||||
/**jsdoc
|
||||
* Returns the Operating Sytem type
|
||||
* @function Test.getOperatingSystemType
|
||||
* @returns {string} "WINDOWS", "MACOS" or "UNKNOWN"
|
||||
*/
|
||||
QString getOperatingSystemType();
|
||||
|
||||
/**jsdoc
|
||||
* Returns the CPU brand
|
||||
*function PlatformInfo.getCPUBrand()
|
||||
* @returns {string} brand of CPU
|
||||
*/
|
||||
QString getCPUBrand();
|
||||
|
||||
/**jsdoc
|
||||
* Returns the number of logical CPU cores
|
||||
*function PlatformInfo.getNumLogicalCores()
|
||||
* @returns {int} number of logical CPU cores
|
||||
*/
|
||||
unsigned int getNumLogicalCores();
|
||||
|
||||
/**jsdoc
|
||||
* Returns the total system memory in megabyte
|
||||
*function PlatformInfo.getTotalSystemMemory()
|
||||
* @returns {int} size of memory in megabytes
|
||||
*/
|
||||
int getTotalSystemMemoryMB();
|
||||
|
||||
/**jsdoc
|
||||
* Returns the graphics card type
|
||||
* @function Test.getGraphicsCardType
|
||||
* @returns {string} graphics card type
|
||||
*/
|
||||
QString getGraphicsCardType();
|
||||
|
||||
/**jsdoc
|
||||
* Returns true if Oculus Rift is connected (looks for hand controllers)
|
||||
* @function Window.hasRift
|
||||
* @returns {boolean} <code>true</code> if running on Windows, otherwise <code>false</code>.*/
|
||||
bool hasRiftControllers();
|
||||
|
||||
/**jsdoc
|
||||
* Returns true if HTC Vive is connected (looks for hand controllers)
|
||||
* @function Window.hasRift
|
||||
* @returns {boolean} <code>true</code> if running on Windows, otherwise <code>false</code>.*/
|
||||
bool hasViveControllers();
|
||||
};
|
||||
|
||||
#endif // hifi_PlatformInfoScriptingInterface_h
|
|
@ -30,7 +30,6 @@
|
|||
|
||||
#include <gl/Context.h>
|
||||
|
||||
#include "BandwidthRecorder.h"
|
||||
#include "Menu.h"
|
||||
#include "Util.h"
|
||||
#include "SequenceNumberStats.h"
|
||||
|
@ -166,20 +165,25 @@ void Stats::updateStats(bool force) {
|
|||
STAT_UPDATE(collisionPicksUpdated, updatedPicks[PickQuery::Collision]);
|
||||
}
|
||||
|
||||
auto bandwidthRecorder = DependencyManager::get<BandwidthRecorder>();
|
||||
STAT_UPDATE(packetInCount, (int)bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond());
|
||||
STAT_UPDATE(packetOutCount, (int)bandwidthRecorder->getCachedTotalAverageOutputPacketsPerSecond());
|
||||
STAT_UPDATE_FLOAT(mbpsIn, (float)bandwidthRecorder->getCachedTotalAverageInputKilobitsPerSecond() / 1000.0f, 0.01f);
|
||||
STAT_UPDATE_FLOAT(mbpsOut, (float)bandwidthRecorder->getCachedTotalAverageOutputKilobitsPerSecond() / 1000.0f, 0.01f);
|
||||
STAT_UPDATE(packetInCount, nodeList->getInboundPPS());
|
||||
STAT_UPDATE(packetOutCount, nodeList->getOutboundPPS());
|
||||
STAT_UPDATE_FLOAT(mbpsIn, nodeList->getInboundKbps() / 1000.0f, 0.01f);
|
||||
STAT_UPDATE_FLOAT(mbpsOut, nodeList->getOutboundKbps() / 1000.0f, 0.01f);
|
||||
|
||||
STAT_UPDATE_FLOAT(assetMbpsIn, (float)bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AssetServer) / 1000.0f, 0.01f);
|
||||
STAT_UPDATE_FLOAT(assetMbpsOut, (float)bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AssetServer) / 1000.0f, 0.01f);
|
||||
|
||||
// Second column: ping
|
||||
SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer);
|
||||
SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer);
|
||||
SharedNodePointer assetServerNode = nodeList->soloNodeOfType(NodeType::AssetServer);
|
||||
SharedNodePointer messageMixerNode = nodeList->soloNodeOfType(NodeType::MessagesMixer);
|
||||
|
||||
if (assetServerNode) {
|
||||
STAT_UPDATE_FLOAT(assetMbpsIn, assetServerNode->getInboundKbps() / 1000.0f, 0.01f);
|
||||
STAT_UPDATE_FLOAT(assetMbpsOut, assetServerNode->getOutboundKbps() / 1000.0f, 0.01f);
|
||||
} else {
|
||||
STAT_UPDATE_FLOAT(assetMbpsIn, 0.0f, 0.01f);
|
||||
STAT_UPDATE_FLOAT(assetMbpsOut, 0.0f, 0.01f);
|
||||
}
|
||||
|
||||
// Second column: ping
|
||||
STAT_UPDATE(audioPing, audioMixerNode ? audioMixerNode->getPingMs() : -1);
|
||||
const int mixerLossRate = (int)roundf(_audioStats->data()->getMixerStream()->lossRateWindow() * 100.0f);
|
||||
const int clientLossRate = (int)roundf(_audioStats->data()->getClientStream()->lossRateWindow() * 100.0f);
|
||||
|
@ -198,7 +202,7 @@ void Stats::updateStats(bool force) {
|
|||
// TODO: this should also support entities
|
||||
if (node->getType() == NodeType::EntityServer) {
|
||||
totalPingOctree += node->getPingMs();
|
||||
totalEntityKbps += node->getInboundBandwidth();
|
||||
totalEntityKbps += node->getInboundKbps();
|
||||
octreeServerCount++;
|
||||
if (pingOctreeMax < node->getPingMs()) {
|
||||
pingOctreeMax = node->getPingMs();
|
||||
|
@ -218,10 +222,10 @@ void Stats::updateStats(bool force) {
|
|||
if (_expanded || force) {
|
||||
SharedNodePointer avatarMixer = nodeList->soloNodeOfType(NodeType::AvatarMixer);
|
||||
if (avatarMixer) {
|
||||
STAT_UPDATE(avatarMixerInKbps, (int)roundf(bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AvatarMixer)));
|
||||
STAT_UPDATE(avatarMixerInPps, (int)roundf(bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AvatarMixer)));
|
||||
STAT_UPDATE(avatarMixerOutKbps, (int)roundf(bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AvatarMixer)));
|
||||
STAT_UPDATE(avatarMixerOutPps, (int)roundf(bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AvatarMixer)));
|
||||
STAT_UPDATE(avatarMixerInKbps, (int)roundf(avatarMixer->getInboundKbps()));
|
||||
STAT_UPDATE(avatarMixerInPps, avatarMixer->getInboundPPS());
|
||||
STAT_UPDATE(avatarMixerOutKbps, (int)roundf(avatarMixer->getOutboundKbps()));
|
||||
STAT_UPDATE(avatarMixerOutPps, avatarMixer->getOutboundPPS());
|
||||
} else {
|
||||
STAT_UPDATE(avatarMixerInKbps, -1);
|
||||
STAT_UPDATE(avatarMixerInPps, -1);
|
||||
|
@ -233,17 +237,15 @@ void Stats::updateStats(bool force) {
|
|||
SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer);
|
||||
auto audioClient = DependencyManager::get<AudioClient>().data();
|
||||
if (audioMixerNode || force) {
|
||||
STAT_UPDATE(audioMixerKbps, (int)roundf(
|
||||
bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AudioMixer) +
|
||||
bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AudioMixer)));
|
||||
STAT_UPDATE(audioMixerPps, (int)roundf(
|
||||
bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AudioMixer) +
|
||||
bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AudioMixer)));
|
||||
STAT_UPDATE(audioMixerKbps, (int)roundf(audioMixerNode->getInboundKbps() +
|
||||
audioMixerNode->getOutboundKbps()));
|
||||
STAT_UPDATE(audioMixerPps, audioMixerNode->getInboundPPS() +
|
||||
audioMixerNode->getOutboundPPS());
|
||||
|
||||
STAT_UPDATE(audioMixerInKbps, (int)roundf(bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AudioMixer)));
|
||||
STAT_UPDATE(audioMixerInPps, (int)roundf(bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AudioMixer)));
|
||||
STAT_UPDATE(audioMixerOutKbps, (int)roundf(bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AudioMixer)));
|
||||
STAT_UPDATE(audioMixerOutPps, (int)roundf(bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AudioMixer)));
|
||||
STAT_UPDATE(audioMixerInKbps, (int)roundf(audioMixerNode->getInboundKbps()));
|
||||
STAT_UPDATE(audioMixerInPps, audioMixerNode->getInboundPPS());
|
||||
STAT_UPDATE(audioMixerOutKbps, (int)roundf(audioMixerNode->getOutboundKbps()));
|
||||
STAT_UPDATE(audioMixerOutPps, audioMixerNode->getOutboundPPS());
|
||||
STAT_UPDATE(audioAudioInboundPPS, (int)audioClient->getAudioInboundPPS());
|
||||
STAT_UPDATE(audioSilentInboundPPS, (int)audioClient->getSilentInboundPPS());
|
||||
STAT_UPDATE(audioOutboundPPS, (int)audioClient->getAudioOutboundPPS());
|
||||
|
|
|
@ -27,6 +27,9 @@ Base3DOverlay::Base3DOverlay() :
|
|||
_drawInFront(false),
|
||||
_drawHUDLayer(false)
|
||||
{
|
||||
// HACK: queryAACube stuff not actually relevant for 3DOverlays, and by setting _queryAACubeSet true here
|
||||
// we can avoid incorrect evaluation for sending updates for entities with 3DOverlays children.
|
||||
_queryAACubeSet = true;
|
||||
}
|
||||
|
||||
Base3DOverlay::Base3DOverlay(const Base3DOverlay* base3DOverlay) :
|
||||
|
@ -41,6 +44,9 @@ Base3DOverlay::Base3DOverlay(const Base3DOverlay* base3DOverlay) :
|
|||
_isVisibleInSecondaryCamera(base3DOverlay->_isVisibleInSecondaryCamera)
|
||||
{
|
||||
setTransform(base3DOverlay->getTransform());
|
||||
// HACK: queryAACube stuff not actually relevant for 3DOverlays, and by setting _queryAACubeSet true here
|
||||
// we can avoid incorrect evaluation for sending updates for entities with 3DOverlays children.
|
||||
_queryAACubeSet = true;
|
||||
}
|
||||
|
||||
QVariantMap convertOverlayLocationFromScriptSemantics(const QVariantMap& properties, bool scalesWithParent) {
|
||||
|
@ -209,6 +215,7 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) {
|
|||
transaction.updateItem(itemID);
|
||||
scene->enqueueTransaction(transaction);
|
||||
}
|
||||
_queryAACubeSet = true; // HACK: just in case some SpatiallyNestable code accidentally set it false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ public:
|
|||
Base3DOverlay(const Base3DOverlay* base3DOverlay);
|
||||
|
||||
void setVisible(bool visible) override;
|
||||
bool queryAACubeNeedsUpdate() const override { return false; } // HACK: queryAACube not relevant for Overlays
|
||||
|
||||
virtual OverlayID getOverlayID() const override { return OverlayID(getID().toString()); }
|
||||
void setOverlayID(OverlayID overlayID) override { setID(overlayID); }
|
||||
|
|
|
@ -109,9 +109,6 @@ void AnimClip::copyFromNetworkAnim() {
|
|||
jointMap.reserve(animJointCount);
|
||||
for (int i = 0; i < animJointCount; i++) {
|
||||
int skeletonJoint = _skeleton->nameToJointIndex(animSkeleton.getJointName(i));
|
||||
if (skeletonJoint == -1) {
|
||||
qCWarning(animation) << "animation contains joint =" << animSkeleton.getJointName(i) << " which is not in the skeleton";
|
||||
}
|
||||
jointMap.push_back(skeletonJoint);
|
||||
}
|
||||
|
||||
|
|
|
@ -425,7 +425,6 @@ int Rig::indexOfJoint(const QString& jointName) const {
|
|||
|
||||
// This is a content error, so we should issue a warning.
|
||||
if (result < 0 && _jointNameWarningCount < MAX_JOINT_NAME_WARNING_COUNT) {
|
||||
qCWarning(animation) << "Rig: Missing joint" << jointName << "in avatar model";
|
||||
_jointNameWarningCount++;
|
||||
}
|
||||
return result;
|
||||
|
|
|
@ -1333,8 +1333,12 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
|
|||
|
||||
} else if (injector->isStereo()) {
|
||||
|
||||
// calculate distance, gain
|
||||
glm::vec3 relativePosition = injector->getPosition() - _positionGetter();
|
||||
float distance = glm::max(glm::length(relativePosition), EPSILON);
|
||||
float gain = gainForSource(distance, injector->getVolume());
|
||||
|
||||
// stereo gets directly mixed into mixBuffer
|
||||
float gain = injector->getVolume();
|
||||
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) {
|
||||
mixBuffer[i] += convertToFloat(_localScratchBuffer[i]) * gain;
|
||||
}
|
||||
|
|
|
@ -695,7 +695,7 @@ static void ifft_radix8_first(complex_t* x, complex_t* y, int n, int p) {
|
|||
// n >= 4
|
||||
static void rfft_post(complex_t* x, const complex_t* w, int n) {
|
||||
|
||||
size_t t = n/4;
|
||||
int t = n/4;
|
||||
assert(t >= 1);
|
||||
|
||||
// NOTE: x[n/2].re is packed into x[0].im
|
||||
|
@ -707,7 +707,7 @@ static void rfft_post(complex_t* x, const complex_t* w, int n) {
|
|||
complex_t* xp0 = &x[1];
|
||||
complex_t* xp1 = &x[n/2 - 1];
|
||||
|
||||
for (size_t i = 0; i < t; i++) {
|
||||
for (int i = 0; i < t; i++) {
|
||||
|
||||
float ar = xp0[i].re;
|
||||
float ai = xp0[i].im;
|
||||
|
@ -743,7 +743,7 @@ static void rfft_post(complex_t* x, const complex_t* w, int n) {
|
|||
// n >= 4
|
||||
static void rifft_pre(complex_t* x, const complex_t* w, int n) {
|
||||
|
||||
size_t t = n/4;
|
||||
int t = n/4;
|
||||
assert(t >= 1);
|
||||
|
||||
// NOTE: x[n/2].re is packed into x[0].im
|
||||
|
@ -755,7 +755,7 @@ static void rifft_pre(complex_t* x, const complex_t* w, int n) {
|
|||
complex_t* xp0 = &x[1];
|
||||
complex_t* xp1 = &x[n/2 - 1];
|
||||
|
||||
for (size_t i = 0; i < t; i++) {
|
||||
for (int i = 0; i < t; i++) {
|
||||
|
||||
float ar = xp0[i].re;
|
||||
float ai = xp0[i].im;
|
||||
|
|
|
@ -973,8 +973,8 @@ FORCEINLINE static void ifft_radix8_first(complex_t* x, complex_t* y, int n, int
|
|||
// n >= 32
|
||||
static void rfft_post(complex_t* x, const complex_t* w, int n) {
|
||||
|
||||
size_t t = n/4;
|
||||
assert(n/4 >= 8); // SIMD8
|
||||
int t = n/4;
|
||||
assert(t >= 8); // SIMD8
|
||||
|
||||
// NOTE: x[n/2].re is packed into x[0].im
|
||||
float tr = x[0].re;
|
||||
|
@ -985,7 +985,7 @@ static void rfft_post(complex_t* x, const complex_t* w, int n) {
|
|||
complex_t* xp0 = &x[1];
|
||||
complex_t* xp1 = &x[n/2 - 8];
|
||||
|
||||
for (size_t i = 0; i < t; i += 8) {
|
||||
for (int i = 0; i < t; i += 8) {
|
||||
|
||||
__m256 z0 = _mm256_loadu_ps(&xp0[i+0].re);
|
||||
__m256 z1 = _mm256_loadu_ps(&xp0[i+4].re);
|
||||
|
@ -1033,8 +1033,8 @@ static void rfft_post(complex_t* x, const complex_t* w, int n) {
|
|||
// n >= 32
|
||||
static void rifft_pre(complex_t* x, const complex_t* w, int n) {
|
||||
|
||||
size_t t = n/4;
|
||||
assert(n/4 >= 8); // SIMD8
|
||||
int t = n/4;
|
||||
assert(t >= 8); // SIMD8
|
||||
|
||||
// NOTE: x[n/2].re is packed into x[0].im
|
||||
float tr = x[0].re;
|
||||
|
@ -1045,7 +1045,7 @@ static void rifft_pre(complex_t* x, const complex_t* w, int n) {
|
|||
complex_t* xp0 = &x[1];
|
||||
complex_t* xp1 = &x[n/2 - 8];
|
||||
|
||||
for (size_t i = 0; i < t; i += 8) {
|
||||
for (int i = 0; i < t; i += 8) {
|
||||
|
||||
__m256 z0 = _mm256_loadu_ps(&xp0[i+0].re);
|
||||
__m256 z1 = _mm256_loadu_ps(&xp0[i+4].re);
|
||||
|
|
|
@ -296,6 +296,7 @@ void Avatar::setTargetScale(float targetScale) {
|
|||
if (_targetScale != newValue) {
|
||||
_targetScale = newValue;
|
||||
_scaleChanged = usecTimestampNow();
|
||||
_avatarScaleChanged = _scaleChanged;
|
||||
_isAnimatingScale = true;
|
||||
|
||||
emit targetScaleChanged(targetScale);
|
||||
|
@ -307,175 +308,16 @@ void Avatar::setAvatarEntityDataChanged(bool value) {
|
|||
_avatarEntityDataHashes.clear();
|
||||
}
|
||||
|
||||
void Avatar::updateAvatarEntities() {
|
||||
PerformanceTimer perfTimer("attachments");
|
||||
|
||||
// AVATAR ENTITY UPDATE FLOW
|
||||
// - if queueEditEntityMessage sees avatarEntity flag it does _myAvatar->updateAvatarEntity()
|
||||
// - 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;
|
||||
}
|
||||
|
||||
if (getID().isNull() ||
|
||||
getID() == AVATAR_SELF_ID ||
|
||||
DependencyManager::get<NodeList>()->getSessionUUID() == QUuid()) {
|
||||
// wait until MyAvatar and this Node gets an ID before doing this. Otherwise, various things go wrong --
|
||||
// things get their parent fixed up from AVATAR_SELF_ID to a null uuid which means "no parent".
|
||||
return;
|
||||
}
|
||||
|
||||
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
||||
if (!entityTree) {
|
||||
return;
|
||||
}
|
||||
|
||||
QScriptEngine scriptEngine;
|
||||
entityTree->withWriteLock([&] {
|
||||
AvatarEntityMap avatarEntities = getAvatarEntityData();
|
||||
AvatarEntityMap::const_iterator dataItr = avatarEntities.begin();
|
||||
while (dataItr != avatarEntities.end()) {
|
||||
// compute hash of data. TODO? cache this?
|
||||
QByteArray data = dataItr.value();
|
||||
uint32_t newHash = qHash(data);
|
||||
|
||||
// check to see if we recognize this hash and whether it was already successfully processed
|
||||
QUuid entityID = dataItr.key();
|
||||
MapOfAvatarEntityDataHashes::iterator stateItr = _avatarEntityDataHashes.find(entityID);
|
||||
if (stateItr != _avatarEntityDataHashes.end()) {
|
||||
if (stateItr.value().success) {
|
||||
if (newHash == stateItr.value().hash) {
|
||||
// data hasn't changed --> nothing to do
|
||||
++dataItr;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// NOTE: if the data was unsuccessful in producing an entity in the past
|
||||
// we will try again just in case something changed (unlikely).
|
||||
// Unfortunately constantly trying to build the entity for this data costs
|
||||
// CPU cycles that we'd rather not spend.
|
||||
// TODO? put a maximum number of tries on this?
|
||||
}
|
||||
} else {
|
||||
// remember this hash for the future
|
||||
stateItr = _avatarEntityDataHashes.insert(entityID, AvatarEntityDataHash(newHash));
|
||||
}
|
||||
++dataItr;
|
||||
|
||||
// see EntityEditPacketSender::queueEditEntityMessage for the other end of this. unpack properties
|
||||
// and either add or update the entity.
|
||||
QJsonDocument jsonProperties = QJsonDocument::fromBinaryData(data);
|
||||
if (!jsonProperties.isObject()) {
|
||||
qCDebug(avatars_renderer) << "got bad avatarEntity json" << QString(data.toHex());
|
||||
continue;
|
||||
}
|
||||
|
||||
QVariant variantProperties = jsonProperties.toVariant();
|
||||
QVariantMap asMap = variantProperties.toMap();
|
||||
QScriptValue scriptProperties = variantMapToScriptValue(asMap, scriptEngine);
|
||||
EntityItemProperties properties;
|
||||
EntityItemPropertiesFromScriptValueHonorReadOnly(scriptProperties, properties);
|
||||
properties.setEntityHostType(entity::HostType::AVATAR);
|
||||
properties.setOwningAvatarID(getID());
|
||||
|
||||
// there's no entity-server to tell us we're the simulation owner, so always set the
|
||||
// simulationOwner to the owningAvatarID and a high priority.
|
||||
properties.setSimulationOwner(getID(), AVATAR_ENTITY_SIMULATION_PRIORITY);
|
||||
|
||||
if (properties.getParentID() == AVATAR_SELF_ID) {
|
||||
properties.setParentID(getID());
|
||||
}
|
||||
|
||||
// NOTE: if this avatar entity is not attached to us, strip its entity script completely...
|
||||
auto attachedScript = properties.getScript();
|
||||
if (!isMyAvatar() && !attachedScript.isEmpty()) {
|
||||
QString noScript;
|
||||
properties.setScript(noScript);
|
||||
}
|
||||
|
||||
auto specifiedHref = properties.getHref();
|
||||
if (!isMyAvatar() && !specifiedHref.isEmpty()) {
|
||||
qCDebug(avatars_renderer) << "removing entity href from avatar attached entity:" << entityID << "old href:" << specifiedHref;
|
||||
QString noHref;
|
||||
properties.setHref(noHref);
|
||||
}
|
||||
|
||||
// When grabbing avatar entities, they are parented to the joint moving them, then when un-grabbed
|
||||
// they go back to the default parent (null uuid). When un-gripped, others saw the entity disappear.
|
||||
// The thinking here is the local position was noticed as changing, but not the parentID (since it is now
|
||||
// back to the default), and the entity flew off somewhere. Marking all changed definitely fixes this,
|
||||
// and seems safe (per Seth).
|
||||
properties.markAllChanged();
|
||||
|
||||
// try to build the entity
|
||||
EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID));
|
||||
bool success = true;
|
||||
if (entity) {
|
||||
if (entityTree->updateEntity(entityID, properties)) {
|
||||
entity->updateLastEditedFromRemote();
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
} else {
|
||||
entity = entityTree->addEntity(entityID, properties);
|
||||
if (!entity) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
stateItr.value().success = success;
|
||||
}
|
||||
|
||||
AvatarEntityIDs recentlyDetachedAvatarEntities = getAndClearRecentlyDetachedIDs();
|
||||
if (!recentlyDetachedAvatarEntities.empty()) {
|
||||
// only lock this thread when absolutely necessary
|
||||
AvatarEntityMap avatarEntityData;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
avatarEntityData = _avatarEntityData;
|
||||
});
|
||||
foreach (auto entityID, recentlyDetachedAvatarEntities) {
|
||||
if (!avatarEntityData.contains(entityID)) {
|
||||
entityTree->deleteEntity(entityID, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
// remove stale data hashes
|
||||
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);
|
||||
}
|
||||
|
||||
void Avatar::removeAvatarEntitiesFromTree() {
|
||||
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
||||
if (entityTree) {
|
||||
QList<QUuid> avatarEntityIDs;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
avatarEntityIDs = _packedAvatarEntityData.keys();
|
||||
});
|
||||
entityTree->withWriteLock([&] {
|
||||
AvatarEntityMap avatarEntities = getAvatarEntityData();
|
||||
for (auto entityID : avatarEntities.keys()) {
|
||||
for (const auto& entityID : avatarEntityIDs) {
|
||||
entityTree->deleteEntity(entityID, true, true);
|
||||
}
|
||||
});
|
||||
|
@ -650,87 +492,6 @@ void Avatar::relayJointDataToChildren() {
|
|||
_reconstructSoftEntitiesJointMap = false;
|
||||
}
|
||||
|
||||
void Avatar::simulate(float deltaTime, bool inView) {
|
||||
PROFILE_RANGE(simulation, "simulate");
|
||||
|
||||
_globalPosition = _transit.isActive() ? _transit.getCurrentPosition() : _serverPosition;
|
||||
if (!hasParent()) {
|
||||
setLocalPosition(_globalPosition);
|
||||
}
|
||||
|
||||
_simulationRate.increment();
|
||||
if (inView) {
|
||||
_simulationInViewRate.increment();
|
||||
}
|
||||
|
||||
PerformanceTimer perfTimer("simulate");
|
||||
{
|
||||
PROFILE_RANGE(simulation, "updateJoints");
|
||||
if (inView) {
|
||||
Head* head = getHead();
|
||||
if (_hasNewJointData || _transit.isActive()) {
|
||||
_skeletonModel->getRig().copyJointsFromJointData(_jointData);
|
||||
glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset());
|
||||
_skeletonModel->getRig().computeExternalPoses(rootTransform);
|
||||
_jointDataSimulationRate.increment();
|
||||
|
||||
_skeletonModel->simulate(deltaTime, true);
|
||||
|
||||
locationChanged(); // joints changed, so if there are any children, update them.
|
||||
_hasNewJointData = false;
|
||||
|
||||
glm::vec3 headPosition = getWorldPosition();
|
||||
if (!_skeletonModel->getHeadPosition(headPosition)) {
|
||||
headPosition = getWorldPosition();
|
||||
}
|
||||
head->setPosition(headPosition);
|
||||
}
|
||||
head->setScale(getModelScale());
|
||||
head->simulate(deltaTime);
|
||||
relayJointDataToChildren();
|
||||
} else {
|
||||
// a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated.
|
||||
_skeletonModel->simulate(deltaTime, false);
|
||||
}
|
||||
_skeletonModelSimulationRate.increment();
|
||||
}
|
||||
|
||||
// update animation for display name fade in/out
|
||||
if ( _displayNameTargetAlpha != _displayNameAlpha) {
|
||||
// the alpha function is
|
||||
// Fade out => alpha(t) = factor ^ t => alpha(t+dt) = alpha(t) * factor^(dt)
|
||||
// Fade in => alpha(t) = 1 - factor^t => alpha(t+dt) = 1-(1-alpha(t))*coef^(dt)
|
||||
// factor^(dt) = coef
|
||||
float coef = pow(DISPLAYNAME_FADE_FACTOR, deltaTime);
|
||||
if (_displayNameTargetAlpha < _displayNameAlpha) {
|
||||
// Fading out
|
||||
_displayNameAlpha *= coef;
|
||||
} else {
|
||||
// Fading in
|
||||
_displayNameAlpha = 1 - (1 - _displayNameAlpha) * coef;
|
||||
}
|
||||
_displayNameAlpha = abs(_displayNameAlpha - _displayNameTargetAlpha) < 0.01f ? _displayNameTargetAlpha : _displayNameAlpha;
|
||||
}
|
||||
|
||||
{
|
||||
PROFILE_RANGE(simulation, "misc");
|
||||
measureMotionDerivatives(deltaTime);
|
||||
simulateAttachments(deltaTime);
|
||||
updatePalms();
|
||||
}
|
||||
{
|
||||
PROFILE_RANGE(simulation, "entities");
|
||||
updateAvatarEntities();
|
||||
}
|
||||
|
||||
{
|
||||
PROFILE_RANGE(simulation, "grabs");
|
||||
updateGrabs();
|
||||
}
|
||||
|
||||
updateFadingStatus();
|
||||
}
|
||||
|
||||
float Avatar::getSimulationRate(const QString& rateName) const {
|
||||
if (rateName == "") {
|
||||
return _simulationRate.rate();
|
||||
|
@ -1045,7 +806,6 @@ void Avatar::render(RenderArgs* renderArgs) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
void Avatar::setEnableMeshVisible(bool isEnabled) {
|
||||
if (_isMeshVisible != isEnabled) {
|
||||
_isMeshVisible = isEnabled;
|
||||
|
|
|
@ -139,9 +139,8 @@ public:
|
|||
typedef render::Payload<AvatarData> Payload;
|
||||
|
||||
void init();
|
||||
void updateAvatarEntities();
|
||||
void removeAvatarEntitiesFromTree();
|
||||
void simulate(float deltaTime, bool inView);
|
||||
virtual void simulate(float deltaTime, bool inView) = 0;
|
||||
virtual void simulateAttachments(float deltaTime);
|
||||
|
||||
virtual void render(RenderArgs* renderArgs);
|
||||
|
@ -156,9 +155,6 @@ public:
|
|||
|
||||
virtual void postUpdate(float deltaTime, const render::ScenePointer& scene);
|
||||
|
||||
//setters
|
||||
void setIsLookAtTarget(const bool isLookAtTarget) { _isLookAtTarget = isLookAtTarget; }
|
||||
bool getIsLookAtTarget() const { return _isLookAtTarget; }
|
||||
//getters
|
||||
bool isInitialized() const { return _initialized; }
|
||||
SkeletonModelPointer getSkeletonModel() { return _skeletonModel; }
|
||||
|
@ -243,8 +239,6 @@ public:
|
|||
static void renderJointConnectingCone(gpu::Batch& batch, glm::vec3 position1, glm::vec3 position2,
|
||||
float radius1, float radius2, const glm::vec4& color);
|
||||
|
||||
virtual void applyCollision(const glm::vec3& contactPoint, const glm::vec3& penetration) { }
|
||||
|
||||
/**jsdoc
|
||||
* Set the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example,
|
||||
* with an offset of <code>{ x: 0, y: 0.1, z: 0 }</code>, your avatar will appear to be raised off the ground slightly.
|
||||
|
@ -552,6 +546,7 @@ protected:
|
|||
glm::vec3 getBodyRightDirection() const { return getWorldOrientation() * IDENTITY_RIGHT; }
|
||||
glm::vec3 getBodyUpDirection() const { return getWorldOrientation() * IDENTITY_UP; }
|
||||
void measureMotionDerivatives(float deltaTime);
|
||||
bool getCollideWithOtherAvatars() const { return _collideWithOtherAvatars; }
|
||||
|
||||
float getSkeletonHeight() const;
|
||||
float getHeadHeight() const;
|
||||
|
@ -595,7 +590,6 @@ protected:
|
|||
int _rightPointerGeometryID { 0 };
|
||||
int _nameRectGeometryID { 0 };
|
||||
bool _initialized { false };
|
||||
bool _isLookAtTarget { false };
|
||||
bool _isAnimatingScale { false };
|
||||
bool _mustFadeIn { false };
|
||||
bool _isFading { false };
|
||||
|
|
|
@ -14,16 +14,33 @@
|
|||
|
||||
#include "AvatarTraits.h"
|
||||
|
||||
// This templated class is admittedly fairly confusing to look at. It is used
|
||||
// to hold some associated value of type T for both simple (non-instanced) and instanced traits.
|
||||
// Most of the complexity comes from the fact that simple and instanced trait types are
|
||||
// handled differently. For each simple trait type there can be a value T, but for
|
||||
// each instance of each instanced trait
|
||||
// (keyed by a TraitInstanceID, which at the time of this writing is a UUID) there can be a value T.
|
||||
// There are separate methods in most cases for simple traits and instanced traits
|
||||
// because of this different behaviour. This class is not used to hold the values
|
||||
// of the traits themselves, but instead an associated value like the latest version
|
||||
// of each trait (see TraitVersions) or a state associated with each trait (like added/changed/deleted).
|
||||
|
||||
namespace AvatarTraits {
|
||||
template<typename T, T defaultValue>
|
||||
class AssociatedTraitValues {
|
||||
public:
|
||||
// constructor that pre-fills _simpleTypes with the default value specified by the template
|
||||
AssociatedTraitValues() : _simpleTypes(FirstInstancedTrait, defaultValue) {}
|
||||
|
||||
/// inserts the given value for the given simple trait type
|
||||
void insert(TraitType type, T value) { _simpleTypes[type] = value; }
|
||||
/// resets the simple trait type value to the default
|
||||
void erase(TraitType type) { _simpleTypes[type] = defaultValue; }
|
||||
|
||||
/// returns a reference to the value for a given instance for a given instanced trait type
|
||||
T& getInstanceValueRef(TraitType traitType, TraitInstanceID instanceID);
|
||||
|
||||
/// inserts the passed value for the given instance for the given instanced trait type
|
||||
void instanceInsert(TraitType traitType, TraitInstanceID instanceID, T value);
|
||||
|
||||
struct InstanceIDValuePair {
|
||||
|
@ -34,24 +51,30 @@ namespace AvatarTraits {
|
|||
};
|
||||
|
||||
using InstanceIDValuePairs = std::vector<InstanceIDValuePair>;
|
||||
|
||||
|
||||
/// returns a vector of InstanceIDValuePair objects for the given instanced trait type
|
||||
InstanceIDValuePairs& getInstanceIDValuePairs(TraitType traitType);
|
||||
|
||||
/// erases the a given instance for a given instanced trait type
|
||||
void instanceErase(TraitType traitType, TraitInstanceID instanceID);
|
||||
/// erases the value for all instances for a given instanced trait type
|
||||
void eraseAllInstances(TraitType traitType);
|
||||
|
||||
// will return defaultValue for instanced traits
|
||||
/// value getters for simple trait types, will be default value if value has been erased or not set
|
||||
T operator[](TraitType traitType) const { return _simpleTypes[traitType]; }
|
||||
T& operator[](TraitType traitType) { return _simpleTypes[traitType]; }
|
||||
|
||||
/// resets all simple trait types to the default value and erases all values for instanced trait types
|
||||
void reset() {
|
||||
std::fill(_simpleTypes.begin(), _simpleTypes.end(), defaultValue);
|
||||
_instancedTypes.clear();
|
||||
}
|
||||
|
||||
/// const iterators for the vector of simple type values
|
||||
typename std::vector<T>::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); }
|
||||
typename std::vector<T>::const_iterator simpleCEnd() const { return _simpleTypes.cend(); }
|
||||
|
||||
/// non-const iterators for the vector of simple type values
|
||||
typename std::vector<T>::iterator simpleBegin() { return _simpleTypes.begin(); }
|
||||
typename std::vector<T>::iterator simpleEnd() { return _simpleTypes.end(); }
|
||||
|
||||
|
@ -64,15 +87,18 @@ namespace AvatarTraits {
|
|||
traitType(traitType), instances({{ instanceID, value }}) {};
|
||||
};
|
||||
|
||||
/// const iterators for the vector of TraitWithInstances objects
|
||||
typename std::vector<TraitWithInstances>::const_iterator instancedCBegin() const { return _instancedTypes.cbegin(); }
|
||||
typename std::vector<TraitWithInstances>::const_iterator instancedCEnd() const { return _instancedTypes.cend(); }
|
||||
|
||||
/// non-const iterators for the vector of TraitWithInstances objects
|
||||
typename std::vector<TraitWithInstances>::iterator instancedBegin() { return _instancedTypes.begin(); }
|
||||
typename std::vector<TraitWithInstances>::iterator instancedEnd() { return _instancedTypes.end(); }
|
||||
|
||||
private:
|
||||
std::vector<T> _simpleTypes;
|
||||
|
||||
/// return the iterator to the matching TraitWithInstances object for a given instanced trait type
|
||||
typename std::vector<TraitWithInstances>::iterator instancesForTrait(TraitType traitType) {
|
||||
return std::find_if(_instancedTypes.begin(), _instancedTypes.end(),
|
||||
[traitType](TraitWithInstances& traitWithInstances){
|
||||
|
@ -83,25 +109,34 @@ namespace AvatarTraits {
|
|||
std::vector<TraitWithInstances> _instancedTypes;
|
||||
};
|
||||
|
||||
/// returns a reference to the InstanceIDValuePairs object for a given instanced trait type
|
||||
template <typename T, T defaultValue>
|
||||
inline typename AssociatedTraitValues<T, defaultValue>::InstanceIDValuePairs&
|
||||
AssociatedTraitValues<T, defaultValue>::getInstanceIDValuePairs(TraitType traitType) {
|
||||
// first check if we already have some values for instances of this trait type
|
||||
auto it = instancesForTrait(traitType);
|
||||
|
||||
if (it != _instancedTypes.end()) {
|
||||
return it->instances;
|
||||
} else {
|
||||
// if we didn't have any values for instances of the instanced trait type
|
||||
// add an empty InstanceIDValuePairs object first and then return the reference to it
|
||||
_instancedTypes.emplace_back(traitType);
|
||||
return _instancedTypes.back().instances;
|
||||
}
|
||||
}
|
||||
|
||||
// returns a reference to value for the given instance of the given instanced trait type
|
||||
template <typename T, T defaultValue>
|
||||
inline T& AssociatedTraitValues<T, defaultValue>::getInstanceValueRef(TraitType traitType, TraitInstanceID instanceID) {
|
||||
// first check if we already have some values for instances of this trait type
|
||||
auto it = instancesForTrait(traitType);
|
||||
|
||||
if (it != _instancedTypes.end()) {
|
||||
// grab the matching vector of instances
|
||||
auto& instancesVector = it->instances;
|
||||
|
||||
// check if we have a value for this specific instance ID
|
||||
auto instanceIt = std::find_if(instancesVector.begin(), instancesVector.end(),
|
||||
[instanceID](InstanceIDValuePair& idValuePair){
|
||||
return idValuePair.id == instanceID;
|
||||
|
@ -109,40 +144,53 @@ namespace AvatarTraits {
|
|||
if (instanceIt != instancesVector.end()) {
|
||||
return instanceIt->value;
|
||||
} else {
|
||||
// no value for this specific instance ID, insert the default value and return it
|
||||
instancesVector.emplace_back(instanceID, defaultValue);
|
||||
return instancesVector.back().value;
|
||||
}
|
||||
} else {
|
||||
// no values for any instances of this trait type
|
||||
// insert the default value for the specific instance for the instanced trait type
|
||||
_instancedTypes.emplace_back(traitType, instanceID, defaultValue);
|
||||
return _instancedTypes.back().instances.back().value;
|
||||
}
|
||||
}
|
||||
|
||||
/// inserts the passed value for the specific instance of the given instanced trait type
|
||||
template <typename T, T defaultValue>
|
||||
inline void AssociatedTraitValues<T, defaultValue>::instanceInsert(TraitType traitType, TraitInstanceID instanceID, T value) {
|
||||
// first check if we already have some instances for this trait type
|
||||
auto it = instancesForTrait(traitType);
|
||||
|
||||
if (it != _instancedTypes.end()) {
|
||||
// found some instances for the instanced trait type, check if our specific instance is one of them
|
||||
auto& instancesVector = it->instances;
|
||||
auto instanceIt = std::find_if(instancesVector.begin(), instancesVector.end(),
|
||||
[instanceID](InstanceIDValuePair& idValuePair){
|
||||
return idValuePair.id == instanceID;
|
||||
});
|
||||
if (instanceIt != instancesVector.end()) {
|
||||
// the instance already existed, update the value
|
||||
instanceIt->value = value;
|
||||
} else {
|
||||
// the instance was not present, emplace the new value
|
||||
instancesVector.emplace_back(instanceID, value);
|
||||
}
|
||||
} else {
|
||||
// there were no existing instances for the given trait type
|
||||
// setup the container for instances and insert the passed value for this instance ID
|
||||
_instancedTypes.emplace_back(traitType, instanceID, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// erases the value for a specific instance of the given instanced trait type
|
||||
template <typename T, T defaultValue>
|
||||
inline void AssociatedTraitValues<T, defaultValue>::instanceErase(TraitType traitType, TraitInstanceID instanceID) {
|
||||
// check if we have any instances at all for this instanced trait type
|
||||
auto it = instancesForTrait(traitType);
|
||||
|
||||
if (it != _instancedTypes.end()) {
|
||||
// we have some instances, erase the value for the passed instance ID if it is present
|
||||
auto& instancesVector = it->instances;
|
||||
instancesVector.erase(std::remove_if(instancesVector.begin(),
|
||||
instancesVector.end(),
|
||||
|
|
|
@ -540,6 +540,10 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
|
|||
if (_headData->getHasProceduralBlinkFaceMovement()) {
|
||||
setAtBit16(flags, PROCEDURAL_BLINK_FACE_MOVEMENT);
|
||||
}
|
||||
// avatar collisions enabled
|
||||
if (_collideWithOtherAvatars) {
|
||||
setAtBit16(flags, COLLIDE_WITH_OTHER_AVATARS);
|
||||
}
|
||||
|
||||
data->flags = flags;
|
||||
destinationBuffer += sizeof(AvatarDataPacket::AdditionalFlags);
|
||||
|
@ -1116,7 +1120,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
|
|||
auto newHasAudioEnabledFaceMovement = oneAtBit16(bitItems, AUDIO_ENABLED_FACE_MOVEMENT);
|
||||
auto newHasProceduralEyeFaceMovement = oneAtBit16(bitItems, PROCEDURAL_EYE_FACE_MOVEMENT);
|
||||
auto newHasProceduralBlinkFaceMovement = oneAtBit16(bitItems, PROCEDURAL_BLINK_FACE_MOVEMENT);
|
||||
|
||||
auto newCollideWithOtherAvatars = oneAtBit16(bitItems, COLLIDE_WITH_OTHER_AVATARS);
|
||||
|
||||
bool keyStateChanged = (_keyState != newKeyState);
|
||||
bool handStateChanged = (_handState != newHandState);
|
||||
|
@ -1125,7 +1129,9 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
|
|||
bool audioEnableFaceMovementChanged = (_headData->getHasAudioEnabledFaceMovement() != newHasAudioEnabledFaceMovement);
|
||||
bool proceduralEyeFaceMovementChanged = (_headData->getHasProceduralEyeFaceMovement() != newHasProceduralEyeFaceMovement);
|
||||
bool proceduralBlinkFaceMovementChanged = (_headData->getHasProceduralBlinkFaceMovement() != newHasProceduralBlinkFaceMovement);
|
||||
bool somethingChanged = keyStateChanged || handStateChanged || faceStateChanged || eyeStateChanged || audioEnableFaceMovementChanged || proceduralEyeFaceMovementChanged || proceduralBlinkFaceMovementChanged;
|
||||
bool collideWithOtherAvatarsChanged = (_collideWithOtherAvatars != newCollideWithOtherAvatars);
|
||||
bool somethingChanged = keyStateChanged || handStateChanged || faceStateChanged || eyeStateChanged || audioEnableFaceMovementChanged ||
|
||||
proceduralEyeFaceMovementChanged || proceduralBlinkFaceMovementChanged || collideWithOtherAvatarsChanged;
|
||||
|
||||
_keyState = newKeyState;
|
||||
_handState = newHandState;
|
||||
|
@ -1134,6 +1140,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
|
|||
_headData->setHasAudioEnabledFaceMovement(newHasAudioEnabledFaceMovement);
|
||||
_headData->setHasProceduralEyeFaceMovement(newHasProceduralEyeFaceMovement);
|
||||
_headData->setHasProceduralBlinkFaceMovement(newHasProceduralBlinkFaceMovement);
|
||||
_collideWithOtherAvatars = newCollideWithOtherAvatars;
|
||||
|
||||
sourceBuffer += sizeof(AvatarDataPacket::AdditionalFlags);
|
||||
|
||||
|
@ -1901,10 +1908,9 @@ qint64 AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitType traitTy
|
|||
|
||||
// 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 (_packedAvatarEntityData.contains(traitInstanceID)) {
|
||||
entityBinaryData = _packedAvatarEntityData[traitInstanceID];
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1980,9 +1986,9 @@ qint64 AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTr
|
|||
qint64 bytesWritten = 0;
|
||||
|
||||
if (traitType == AvatarTraits::AvatarEntity) {
|
||||
packAvatarEntityTraitInstance(traitType, traitInstanceID, destination, traitVersion);
|
||||
bytesWritten += packAvatarEntityTraitInstance(traitType, traitInstanceID, destination, traitVersion);
|
||||
} else if (traitType == AvatarTraits::Grab) {
|
||||
packGrabTraitInstance(traitType, traitInstanceID, destination, traitVersion);
|
||||
bytesWritten += packGrabTraitInstance(traitType, traitInstanceID, destination, traitVersion);
|
||||
}
|
||||
|
||||
return bytesWritten;
|
||||
|
@ -1991,7 +1997,7 @@ qint64 AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTr
|
|||
void AvatarData::prepareResetTraitInstances() {
|
||||
if (_clientTraitsHandler) {
|
||||
_avatarEntitiesLock.withReadLock([this]{
|
||||
foreach (auto entityID, _avatarEntityData.keys()) {
|
||||
foreach (auto entityID, _packedAvatarEntityData.keys()) {
|
||||
_clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID);
|
||||
}
|
||||
foreach (auto grabID, _avatarGrabData.keys()) {
|
||||
|
@ -2012,7 +2018,7 @@ void AvatarData::processTrait(AvatarTraits::TraitType traitType, QByteArray trai
|
|||
void AvatarData::processTraitInstance(AvatarTraits::TraitType traitType,
|
||||
AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData) {
|
||||
if (traitType == AvatarTraits::AvatarEntity) {
|
||||
updateAvatarEntity(instanceID, traitBinaryData);
|
||||
storeAvatarEntityDataPayload(instanceID, traitBinaryData);
|
||||
} else if (traitType == AvatarTraits::Grab) {
|
||||
updateAvatarGrabData(instanceID, traitBinaryData);
|
||||
}
|
||||
|
@ -2360,7 +2366,7 @@ void AvatarData::setRecordingBasis(std::shared_ptr<Transform> recordingBasis) {
|
|||
void AvatarData::createRecordingIDs() {
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
_avatarEntityForRecording.clear();
|
||||
for (int i = 0; i < _avatarEntityData.size(); i++) {
|
||||
for (int i = 0; i < _packedAvatarEntityData.size(); i++) {
|
||||
_avatarEntityForRecording.insert(QUuid::createUuid());
|
||||
}
|
||||
});
|
||||
|
@ -2415,6 +2421,10 @@ JointData jointDataFromJsonValue(int version, const QJsonValue& json) {
|
|||
return result;
|
||||
}
|
||||
|
||||
void AvatarData::avatarEntityDataToJson(QJsonObject& root) const {
|
||||
// overridden where needed
|
||||
}
|
||||
|
||||
QJsonObject AvatarData::toJson() const {
|
||||
QJsonObject root;
|
||||
|
||||
|
@ -2426,20 +2436,8 @@ QJsonObject AvatarData::toJson() const {
|
|||
if (!getDisplayName().isEmpty()) {
|
||||
root[JSON_AVATAR_DISPLAY_NAME] = getDisplayName();
|
||||
}
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
if (!_avatarEntityData.empty()) {
|
||||
QJsonArray avatarEntityJson;
|
||||
int entityCount = 0;
|
||||
for (auto entityID : _avatarEntityData.keys()) {
|
||||
QVariantMap entityData;
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
avatarEntityDataToJson(root);
|
||||
|
||||
auto recordingBasis = getRecordingBasis();
|
||||
bool success;
|
||||
|
@ -2561,9 +2559,9 @@ void AvatarData::fromJson(const QJsonObject& json, bool useFrameSkeleton) {
|
|||
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);
|
||||
QUuid id = entityData.value("id").toUuid();
|
||||
QByteArray data = QByteArray::fromBase64(entityData.value("properties").toByteArray());
|
||||
updateAvatarEntity(id, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2745,17 +2743,15 @@ void AvatarData::setAttachmentsVariant(const QVariantList& variant) {
|
|||
setAttachmentData(newAttachments);
|
||||
}
|
||||
|
||||
const int MAX_NUM_AVATAR_ENTITIES = 42;
|
||||
|
||||
void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) {
|
||||
void AvatarData::storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& data) {
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
AvatarEntityMap::iterator itr = _avatarEntityData.find(entityID);
|
||||
if (itr == _avatarEntityData.end()) {
|
||||
if (_avatarEntityData.size() < MAX_NUM_AVATAR_ENTITIES) {
|
||||
_avatarEntityData.insert(entityID, entityData);
|
||||
PackedAvatarEntityMap::iterator itr = _packedAvatarEntityData.find(entityID);
|
||||
if (itr == _packedAvatarEntityData.end()) {
|
||||
if (_packedAvatarEntityData.size() < MAX_NUM_AVATAR_ENTITIES) {
|
||||
_packedAvatarEntityData.insert(entityID, data);
|
||||
}
|
||||
} else {
|
||||
itr.value() = entityData;
|
||||
itr.value() = data;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -2768,15 +2764,20 @@ void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& ent
|
|||
}
|
||||
}
|
||||
|
||||
void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) {
|
||||
// overridden where needed
|
||||
// expects 'entityData' to be a JavaScript EntityItemProperties Object in QByteArray form
|
||||
}
|
||||
|
||||
void AvatarData::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree) {
|
||||
|
||||
bool removedEntity = false;
|
||||
|
||||
_avatarEntitiesLock.withWriteLock([this, &removedEntity, &entityID] {
|
||||
removedEntity = _avatarEntityData.remove(entityID);
|
||||
removedEntity = _packedAvatarEntityData.remove(entityID);
|
||||
});
|
||||
|
||||
insertDetachedEntityID(entityID);
|
||||
insertRemovedEntityID(entityID);
|
||||
|
||||
if (removedEntity && _clientTraitsHandler) {
|
||||
// we have a client traits handler, so we need to mark this removed instance trait as deleted
|
||||
|
@ -2786,75 +2787,29 @@ void AvatarData::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFr
|
|||
}
|
||||
|
||||
AvatarEntityMap AvatarData::getAvatarEntityData() const {
|
||||
AvatarEntityMap result;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
result = _avatarEntityData;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void AvatarData::insertDetachedEntityID(const QUuid entityID) {
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
_avatarEntityDetached.insert(entityID);
|
||||
});
|
||||
|
||||
_avatarEntityDataChanged = true;
|
||||
// overridden where needed
|
||||
// NOTE: the return value is expected to be a map of unfortunately-formatted-binary-blobs
|
||||
return AvatarEntityMap();
|
||||
}
|
||||
|
||||
void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
|
||||
if (avatarEntityData.size() > MAX_NUM_AVATAR_ENTITIES) {
|
||||
// the data is suspect
|
||||
qCDebug(avatars) << "discard suspect AvatarEntityData with size =" << avatarEntityData.size();
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<QUuid> deletedEntityIDs;
|
||||
QList<QUuid> updatedEntityIDs;
|
||||
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
if (_avatarEntityData != avatarEntityData) {
|
||||
|
||||
// keep track of entities that were attached to this avatar but no longer are
|
||||
AvatarEntityIDs previousAvatarEntityIDs = QSet<QUuid>::fromList(_avatarEntityData.keys());
|
||||
|
||||
_avatarEntityData = avatarEntityData;
|
||||
setAvatarEntityDataChanged(true);
|
||||
|
||||
deletedEntityIDs.reserve(previousAvatarEntityIDs.size());
|
||||
|
||||
foreach (auto entityID, previousAvatarEntityIDs) {
|
||||
if (!_avatarEntityData.contains(entityID)) {
|
||||
_avatarEntityDetached.insert(entityID);
|
||||
deletedEntityIDs.push_back(entityID);
|
||||
}
|
||||
}
|
||||
|
||||
updatedEntityIDs = _avatarEntityData.keys();
|
||||
}
|
||||
});
|
||||
|
||||
if (_clientTraitsHandler) {
|
||||
// we have a client traits handler
|
||||
|
||||
// flag removed entities as deleted so that changes are sent next frame
|
||||
for (auto& deletedEntityID : deletedEntityIDs) {
|
||||
_clientTraitsHandler->markInstancedTraitDeleted(AvatarTraits::AvatarEntity, deletedEntityID);
|
||||
}
|
||||
|
||||
// flag any updated or created entities so that we send changes for them next frame
|
||||
for (auto& entityID : updatedEntityIDs) {
|
||||
_clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// overridden where needed
|
||||
// avatarEntityData is expected to be a map of QByteArrays
|
||||
// each QByteArray represents an EntityItemProperties object from JavaScript
|
||||
}
|
||||
|
||||
AvatarEntityIDs AvatarData::getAndClearRecentlyDetachedIDs() {
|
||||
void AvatarData::insertRemovedEntityID(const QUuid entityID) {
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
_avatarEntityRemoved.insert(entityID);
|
||||
});
|
||||
_avatarEntityDataChanged = true;
|
||||
}
|
||||
|
||||
AvatarEntityIDs AvatarData::getAndClearRecentlyRemovedIDs() {
|
||||
AvatarEntityIDs result;
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
result = _avatarEntityDetached;
|
||||
_avatarEntityDetached.clear();
|
||||
result = _avatarEntityRemoved;
|
||||
_avatarEntityRemoved.clear();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ using AvatarWeakPointer = std::weak_ptr<AvatarData>;
|
|||
using AvatarHash = QHash<QUuid, AvatarSharedPointer>;
|
||||
|
||||
using AvatarEntityMap = QMap<QUuid, QByteArray>;
|
||||
using PackedAvatarEntityMap = QMap<QUuid, QByteArray>; // similar to AvatarEntityMap, but different internal format
|
||||
using AvatarEntityIDs = QSet<QUuid>;
|
||||
|
||||
using AvatarGrabDataMap = QMap<QUuid, QByteArray>;
|
||||
|
@ -71,6 +72,8 @@ using AvatarGrabMap = QMap<QUuid, GrabPointer>;
|
|||
|
||||
using AvatarDataSequenceNumber = uint16_t;
|
||||
|
||||
const int MAX_NUM_AVATAR_ENTITIES = 42;
|
||||
|
||||
// avatar motion behaviors
|
||||
const quint32 AVATAR_MOTION_ACTION_MOTOR_ENABLED = 1U << 0;
|
||||
const quint32 AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED = 1U << 1;
|
||||
|
@ -110,6 +113,7 @@ const int HAND_STATE_FINGER_POINTING_BIT = 7; // 8th bit
|
|||
const int AUDIO_ENABLED_FACE_MOVEMENT = 8; // 9th bit
|
||||
const int PROCEDURAL_EYE_FACE_MOVEMENT = 9; // 10th bit
|
||||
const int PROCEDURAL_BLINK_FACE_MOVEMENT = 10; // 11th bit
|
||||
const int COLLIDE_WITH_OTHER_AVATARS = 11; // 12th bit
|
||||
|
||||
|
||||
const char HAND_STATE_NULL = 0;
|
||||
|
@ -951,19 +955,20 @@ public:
|
|||
// FIXME: Can this name be improved? Can it be deprecated?
|
||||
Q_INVOKABLE virtual void setAttachmentsVariant(const QVariantList& variant);
|
||||
|
||||
virtual void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload);
|
||||
|
||||
/**jsdoc
|
||||
* @function MyAvatar.updateAvatarEntity
|
||||
* @param {Uuid} entityID
|
||||
* @param {string} entityData
|
||||
*/
|
||||
Q_INVOKABLE void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData);
|
||||
Q_INVOKABLE virtual void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData);
|
||||
|
||||
/**jsdoc
|
||||
* @function MyAvatar.clearAvatarEntity
|
||||
* @param {Uuid} entityID
|
||||
*/
|
||||
Q_INVOKABLE void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true);
|
||||
Q_INVOKABLE virtual void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true);
|
||||
|
||||
|
||||
/**jsdoc
|
||||
|
@ -1124,6 +1129,7 @@ public:
|
|||
TransformPointer getRecordingBasis() const;
|
||||
void setRecordingBasis(TransformPointer recordingBasis = TransformPointer());
|
||||
void createRecordingIDs();
|
||||
virtual void avatarEntityDataToJson(QJsonObject& root) const;
|
||||
QJsonObject toJson() const;
|
||||
void fromJson(const QJsonObject& json, bool useFrameSkeleton = true);
|
||||
|
||||
|
@ -1135,17 +1141,16 @@ public:
|
|||
* @function MyAvatar.getAvatarEntityData
|
||||
* @returns {object}
|
||||
*/
|
||||
Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const;
|
||||
Q_INVOKABLE virtual AvatarEntityMap getAvatarEntityData() const;
|
||||
|
||||
/**jsdoc
|
||||
* @function MyAvatar.setAvatarEntityData
|
||||
* @param {object} avatarEntityData
|
||||
*/
|
||||
Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData);
|
||||
Q_INVOKABLE virtual void setAvatarEntityData(const AvatarEntityMap& avatarEntityData);
|
||||
|
||||
virtual void setAvatarEntityDataChanged(bool value) { _avatarEntityDataChanged = value; }
|
||||
void insertDetachedEntityID(const QUuid entityID);
|
||||
AvatarEntityIDs getAndClearRecentlyDetachedIDs();
|
||||
AvatarEntityIDs getAndClearRecentlyRemovedIDs();
|
||||
|
||||
/**jsdoc
|
||||
* @function MyAvatar.getSensorToWorldMatrix
|
||||
|
@ -1332,6 +1337,7 @@ public slots:
|
|||
void resetLastSent() { _lastToByteArray = 0; }
|
||||
|
||||
protected:
|
||||
void insertRemovedEntityID(const QUuid entityID);
|
||||
void lazyInitHeadData() const;
|
||||
|
||||
float getDistanceBasedMinRotationDOT(glm::vec3 viewerPosition) const;
|
||||
|
@ -1460,9 +1466,9 @@ protected:
|
|||
AABox _defaultBubbleBox;
|
||||
|
||||
mutable ReadWriteLockable _avatarEntitiesLock;
|
||||
AvatarEntityIDs _avatarEntityDetached; // recently detached from this avatar
|
||||
AvatarEntityIDs _avatarEntityRemoved; // recently removed AvatarEntity ids
|
||||
AvatarEntityIDs _avatarEntityForRecording; // create new entities id for avatar recording
|
||||
AvatarEntityMap _avatarEntityData;
|
||||
PackedAvatarEntityMap _packedAvatarEntityData;
|
||||
bool _avatarEntityDataChanged { false };
|
||||
|
||||
mutable ReadWriteLockable _avatarGrabsLock;
|
||||
|
@ -1495,6 +1501,7 @@ protected:
|
|||
int _replicaIndex { 0 };
|
||||
bool _isNewAvatar { true };
|
||||
bool _isClientAvatar { false };
|
||||
bool _collideWithOtherAvatars { true };
|
||||
|
||||
// null unless MyAvatar or ScriptableAvatar sending traits data to mixer
|
||||
std::unique_ptr<ClientTraitsHandler, LaterDeleter> _clientTraitsHandler;
|
||||
|
|
|
@ -328,6 +328,19 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
|
|||
}
|
||||
|
||||
void AvatarHashMap::processBulkAvatarTraits(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
|
||||
AvatarTraits::TraitMessageSequence seq;
|
||||
|
||||
message->readPrimitive(&seq);
|
||||
|
||||
auto traitsAckPacket = NLPacket::create(PacketType::BulkAvatarTraitsAck, sizeof(AvatarTraits::TraitMessageSequence), true);
|
||||
traitsAckPacket->writePrimitive(seq);
|
||||
auto nodeList = DependencyManager::get<LimitedNodeList>();
|
||||
SharedNodePointer avatarMixer = nodeList->soloNodeOfType(NodeType::AvatarMixer);
|
||||
if (!avatarMixer.isNull()) {
|
||||
// we have a mixer to send to, acknowledge that we received these
|
||||
// traits.
|
||||
nodeList->sendPacket(std::move(traitsAckPacket), *avatarMixer);
|
||||
}
|
||||
|
||||
while (message->getBytesLeftToRead()) {
|
||||
// read the avatar ID to figure out which avatar this is for
|
||||
|
|
|
@ -42,6 +42,10 @@ namespace AvatarTraits {
|
|||
const TraitWireSize DELETED_TRAIT_SIZE = -1;
|
||||
const TraitWireSize MAXIMUM_TRAIT_SIZE = INT16_MAX;
|
||||
|
||||
using TraitMessageSequence = int64_t;
|
||||
const TraitMessageSequence FIRST_TRAIT_SEQUENCE = 0;
|
||||
const TraitMessageSequence MAX_TRAIT_SEQUENCE = INT64_MAX;
|
||||
|
||||
inline qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination,
|
||||
TraitVersion traitVersion = NULL_TRAIT_VERSION) {
|
||||
qint64 bytesWritten = 0;
|
||||
|
|
13
libraries/avatars/src/ProjectFile.h
Normal file
13
libraries/avatars/src/ProjectFile.h
Normal file
|
@ -0,0 +1,13 @@
|
|||
#ifndef hifi_AvatarProjectFile_h
|
||||
#define hifi_AvatarProjectFile_h
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class ProjectFilePath {
|
||||
Q_GADGET;
|
||||
public:
|
||||
QString absolutePath;
|
||||
QString relativePath;
|
||||
};
|
||||
|
||||
#endif // hifi_AvatarProjectFile_h
|
|
@ -52,11 +52,17 @@ namespace controller {
|
|||
* <tr><td><code>TranslateZ</code></td><td>number</td><td>number</td><td>Move the user's avatar in the direction of its
|
||||
* z-axis, if the camera isn't in independent or mirror modes.</td></tr>
|
||||
* <tr><td><code>Pitch</code></td><td>number</td><td>number</td><td>Rotate the user's avatar head and attached camera
|
||||
* about its negative x-axis (i.e., positive values pitch down), if the camera isn't in HMD, independent, or mirror
|
||||
* modes.</td></tr>
|
||||
* <tr><td><code>Yaw</code></td><td>number</td><td>number</td><td>Rotate the user's avatar about its y-axis, if the
|
||||
* camera isn't in independent or mirror modes.</td></tr>
|
||||
* about its negative x-axis (i.e., positive values pitch down) at a rate proportional to the control value, if the
|
||||
* camera isn't in HMD, independent, or mirror modes.</td></tr>
|
||||
* <tr><td><code>Yaw</code></td><td>number</td><td>number</td><td>Rotate the user's avatar about its y-axis at a rate
|
||||
* proportional to the control value, if the camera isn't in independent or mirror modes.</td></tr>
|
||||
* <tr><td><code>Roll</code></td><td>number</td><td>number</td><td>No action.</td></tr>
|
||||
* <tr><td><code>DeltaPitch</code></td><td>number</td><td>number</td><td>Rotate the user's avatar head and attached
|
||||
* camera about its negative x-axis (i.e., positive values pitch down) by an amount proportional to the control value,
|
||||
* if the camera isn't in HMD, independent, or mirror modes.</td></tr>
|
||||
* <tr><td><code>DeltaYaw</code></td><td>number</td><td>number</td><td>Rotate the user's avatar about its y-axis by an
|
||||
* amount proportional to the control value, if the camera isn't in independent or mirror modes.</td></tr>
|
||||
* <tr><td><code>DeltaRoll</code></td><td>number</td><td>number</td><td>No action.</td></tr>
|
||||
* <tr><td><code>StepTranslateX</code></td><td>number</td><td>number</td><td>No action.</td></tr>
|
||||
* <tr><td><code>StepTranslateY</code></td><td>number</td><td>number</td><td>No action.</td></tr>
|
||||
* <tr><td><code>StepTranslateZ</code></td><td>number</td><td>number</td><td>No action.</td></tr>
|
||||
|
@ -318,6 +324,9 @@ namespace controller {
|
|||
makeAxisPair(Action::ROLL, "Roll"),
|
||||
makeAxisPair(Action::PITCH, "Pitch"),
|
||||
makeAxisPair(Action::YAW, "Yaw"),
|
||||
makeAxisPair(Action::DELTA_YAW, "DeltaYaw"),
|
||||
makeAxisPair(Action::DELTA_PITCH, "DeltaPitch"),
|
||||
makeAxisPair(Action::DELTA_ROLL, "DeltaRoll"),
|
||||
makeAxisPair(Action::STEP_YAW, "StepYaw"),
|
||||
makeAxisPair(Action::STEP_PITCH, "StepPitch"),
|
||||
makeAxisPair(Action::STEP_ROLL, "StepRoll"),
|
||||
|
|
|
@ -27,6 +27,10 @@ enum class Action {
|
|||
ROTATE_Y, YAW = ROTATE_Y,
|
||||
ROTATE_Z, ROLL = ROTATE_Z,
|
||||
|
||||
DELTA_PITCH,
|
||||
DELTA_YAW,
|
||||
DELTA_ROLL,
|
||||
|
||||
STEP_YAW,
|
||||
// FIXME does this have a use case?
|
||||
STEP_PITCH,
|
||||
|
|
21
libraries/controllers/src/controllers/AxisValue.cpp
Normal file
21
libraries/controllers/src/controllers/AxisValue.cpp
Normal file
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// AxisValue.cpp
|
||||
//
|
||||
// Created by David Rowe on 14 Dec 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
|
||||
//
|
||||
|
||||
#include "AxisValue.h"
|
||||
|
||||
namespace controller {
|
||||
|
||||
AxisValue::AxisValue(const float value, const quint64 timestamp) :
|
||||
value(value), timestamp(timestamp) { }
|
||||
|
||||
bool AxisValue::operator==(const AxisValue& right) const {
|
||||
return value == right.value && timestamp == right.timestamp;
|
||||
}
|
||||
}
|
34
libraries/controllers/src/controllers/AxisValue.h
Normal file
34
libraries/controllers/src/controllers/AxisValue.h
Normal file
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// AxisValue.h
|
||||
//
|
||||
// Created by David Rowe on 13 Dec 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
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#ifndef hifi_controllers_AxisValue_h
|
||||
#define hifi_controllers_AxisValue_h
|
||||
|
||||
#include <QtCore/qglobal.h>
|
||||
|
||||
namespace controller {
|
||||
|
||||
struct AxisValue {
|
||||
public:
|
||||
float value { 0.0f };
|
||||
// The value can be timestamped to determine if consecutive identical values should be output (e.g., mouse movement).
|
||||
quint64 timestamp { 0 };
|
||||
|
||||
AxisValue() {}
|
||||
AxisValue(const float value, const quint64 timestamp);
|
||||
|
||||
bool operator ==(const AxisValue& right) const;
|
||||
bool operator !=(const AxisValue& right) const { return !(*this == right); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // hifi_controllers_AxisValue_h
|
|
@ -26,12 +26,12 @@ namespace controller {
|
|||
return 0.0f;
|
||||
}
|
||||
|
||||
float InputDevice::getAxis(int channel) const {
|
||||
AxisValue InputDevice::getAxis(int channel) const {
|
||||
auto axis = _axisStateMap.find(channel);
|
||||
if (axis != _axisStateMap.end()) {
|
||||
return (*axis).second;
|
||||
} else {
|
||||
return 0.0f;
|
||||
return AxisValue();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,26 +68,25 @@ namespace controller {
|
|||
return Input::NamedPair(makeInput(pose), name);
|
||||
}
|
||||
|
||||
float InputDevice::getValue(ChannelType channelType, uint16_t channel) const {
|
||||
AxisValue InputDevice::getValue(ChannelType channelType, uint16_t channel) const {
|
||||
switch (channelType) {
|
||||
case ChannelType::AXIS:
|
||||
return getAxis(channel);
|
||||
|
||||
case ChannelType::BUTTON:
|
||||
return getButton(channel);
|
||||
return { getButton(channel), 0 };
|
||||
|
||||
case ChannelType::POSE:
|
||||
return getPose(channel).valid ? 1.0f : 0.0f;
|
||||
return { getPose(channel).valid ? 1.0f : 0.0f, 0 };
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
return { 0.0f, 0 };
|
||||
}
|
||||
|
||||
|
||||
float InputDevice::getValue(const Input& input) const {
|
||||
AxisValue InputDevice::getValue(const Input& input) const {
|
||||
return getValue(input.getType(), input.channel);
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
#include <QtCore/QString>
|
||||
|
||||
#include "AxisValue.h"
|
||||
#include "Pose.h"
|
||||
#include "Input.h"
|
||||
#include "StandardControls.h"
|
||||
|
@ -103,16 +104,16 @@ public:
|
|||
using Pointer = std::shared_ptr<InputDevice>;
|
||||
|
||||
typedef std::unordered_set<int> ButtonPressedMap;
|
||||
typedef std::map<int, float> AxisStateMap;
|
||||
typedef std::map<int, AxisValue> AxisStateMap;
|
||||
typedef std::map<int, Pose> PoseStateMap;
|
||||
|
||||
// Get current state for each channel
|
||||
float getButton(int channel) const;
|
||||
float getAxis(int channel) const;
|
||||
AxisValue getAxis(int channel) const;
|
||||
Pose getPose(int channel) const;
|
||||
|
||||
float getValue(const Input& input) const;
|
||||
float getValue(ChannelType channelType, uint16_t channel) const;
|
||||
AxisValue getValue(const Input& input) const;
|
||||
AxisValue getValue(ChannelType channelType, uint16_t channel) const;
|
||||
Pose getPoseValue(uint16_t channel) const;
|
||||
|
||||
const QString& getName() const { return _name; }
|
||||
|
|
|
@ -297,6 +297,13 @@ namespace controller {
|
|||
return 0.0f;
|
||||
}
|
||||
|
||||
InputRecorder::ActionStates InputRecorder::getActionstates() {
|
||||
if (_actionStateList.size() > 0) {
|
||||
return _actionStateList[_playCount];
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
controller::Pose InputRecorder::getPoseState(const QString& action) {
|
||||
if (_poseStateList.size() > 0) {
|
||||
return _poseStateList[_playCount][action];
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue