Merge branch 'master' into oculusLoginFeature
2
.gitignore
vendored
|
@ -98,5 +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"
|
||||
|
||||
|
||||
|
|
|
@ -11,9 +11,10 @@ setup_memory_debugger()
|
|||
|
||||
# link in the shared libraries
|
||||
link_hifi_libraries(
|
||||
audio avatars octree gpu graphics fbx hfm entities
|
||||
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)
|
||||
|
|
|
@ -276,6 +276,7 @@ void AssignmentClientMonitor::checkSpares() {
|
|||
|
||||
// Spawn or kill children, as needed. If --min or --max weren't specified, allow the child count
|
||||
// to drift up or down as far as needed.
|
||||
|
||||
if (spareCount < 1 || totalCount < _minAssignmentClientForks) {
|
||||
if (!_maxAssignmentClientForks || totalCount < _maxAssignmentClientForks) {
|
||||
spawnChildClient();
|
||||
|
@ -307,7 +308,7 @@ void AssignmentClientMonitor::handleChildStatusPacket(QSharedPointer<ReceivedMes
|
|||
AssignmentClientChildData* childData = nullptr;
|
||||
|
||||
if (!matchingNode) {
|
||||
// The parent only expects to be talking with prorams running on this same machine.
|
||||
// The parent only expects to be talking with programs running on this same machine.
|
||||
if (senderSockAddr.getAddress() == QHostAddress::LocalHost ||
|
||||
senderSockAddr.getAddress() == QHostAddress::LocalHostIPv6) {
|
||||
|
||||
|
@ -316,9 +317,9 @@ void AssignmentClientMonitor::handleChildStatusPacket(QSharedPointer<ReceivedMes
|
|||
matchingNode = DependencyManager::get<LimitedNodeList>()->addOrUpdateNode(senderID, NodeType::Unassigned,
|
||||
senderSockAddr, senderSockAddr);
|
||||
|
||||
auto childData = std::unique_ptr<AssignmentClientChildData>
|
||||
auto newChildData = std::unique_ptr<AssignmentClientChildData>
|
||||
{ new AssignmentClientChildData(Assignment::Type::AllTypes) };
|
||||
matchingNode->setLinkedData(std::move(childData));
|
||||
matchingNode->setLinkedData(std::move(newChildData));
|
||||
} else {
|
||||
// tell unknown assignment-client child to exit.
|
||||
qDebug() << "Asking unknown child at" << senderSockAddr << "to exit.";
|
||||
|
@ -329,9 +330,8 @@ void AssignmentClientMonitor::handleChildStatusPacket(QSharedPointer<ReceivedMes
|
|||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
childData = dynamic_cast<AssignmentClientChildData*>(matchingNode->getLinkedData());
|
||||
}
|
||||
childData = dynamic_cast<AssignmentClientChildData*>(matchingNode->getLinkedData());
|
||||
|
||||
if (childData) {
|
||||
// update our records about how to reach this child
|
||||
|
|
|
@ -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,18 +144,20 @@ 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;
|
||||
}
|
||||
|
||||
if (traitType == AvatarTraits::AvatarEntity) {
|
||||
if (traitType == AvatarTraits::AvatarEntity ||
|
||||
traitType == AvatarTraits::Grab) {
|
||||
auto& instanceVersionRef = _lastReceivedTraitVersions.getInstanceValueRef(traitType, instanceID);
|
||||
|
||||
if (packetTraitVersion > instanceVersionRef) {
|
||||
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
|
||||
|
@ -168,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;
|
||||
}
|
||||
}
|
||||
|
@ -179,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;
|
||||
|
||||
|
@ -281,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);
|
||||
|
@ -316,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;
|
||||
|
@ -337,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;
|
||||
});
|
||||
|
||||
|
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 39 KiB |
|
@ -1,16 +1,2 @@
|
|||
{
|
||||
"RenderMainView": {
|
||||
"RenderShadowTask": {
|
||||
"Enabled": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"RenderDeferredTask": {
|
||||
"AmbientOcclusion": {
|
||||
"Enabled": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" },
|
||||
|
||||
|
|
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 Width: | Height: | Size: 824 B |
BIN
interface/resources/icons/loader-snake-256-wf.gif
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
interface/resources/icons/loader-snake-256.gif
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
interface/resources/images/loader-snake-128.png
Normal file
After Width: | Height: | 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;
|
||||
|
|
|
@ -189,15 +189,17 @@ Windows.ScrollingWindow {
|
|||
var grabbable = MenuInterface.isOptionChecked("Create Entities As Grabbable (except Zones, Particles, and Lights)");
|
||||
|
||||
if (defaultURL.endsWith(".jpg") || defaultURL.endsWith(".png")) {
|
||||
var name = assetProxyModel.data(treeView.selection.currentIndex);
|
||||
var modelURL = "https://hifi-content.s3.amazonaws.com/DomainContent/production/default-image-model.fbx";
|
||||
var textures = JSON.stringify({ "tex.picture": defaultURL});
|
||||
var shapeType = "box";
|
||||
var dynamic = false;
|
||||
var collisionless = true;
|
||||
var position = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getForward(MyAvatar.orientation)));
|
||||
var gravity = Vec3.multiply(Vec3.fromPolar(Math.PI / 2, 0), 0);
|
||||
Entities.addModelEntity(name, modelURL, textures, shapeType, dynamic, collisionless, grabbable, position, gravity);
|
||||
Entities.addEntity({
|
||||
type: "Image",
|
||||
name: assetProxyModel.data(treeView.selection.currentIndex),
|
||||
imageURL: defaultURL,
|
||||
keepAspectRatio: false,
|
||||
dynamic: false,
|
||||
collisionless: true,
|
||||
grabbable: grabbable,
|
||||
position: Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getForward(MyAvatar.orientation))),
|
||||
gravity: Vec3.multiply(Vec3.fromPolar(Math.PI / 2, 0), 0)
|
||||
});
|
||||
} else {
|
||||
var SHAPE_TYPE_NONE = 0;
|
||||
var SHAPE_TYPE_SIMPLE_HULL = 1;
|
||||
|
|
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,2 @@
|
|||
module AvatarPackager
|
||||
singleton AvatarPackagerState 1.0 AvatarPackagerState.qml
|
|
@ -75,6 +75,10 @@ Rectangle {
|
|||
if(materialUrlOrJson) {
|
||||
wearable.text = 'Material: ' + materialUrlOrJson;
|
||||
}
|
||||
} else if (wearable.sourceUrl) {
|
||||
wearable.text = extractTitleFromUrl(wearable.sourceUrl);
|
||||
} else if (wearable.name) {
|
||||
wearable.text = wearable.name;
|
||||
}
|
||||
wearablesCombobox.model.append(wearable);
|
||||
}
|
||||
|
@ -153,7 +157,7 @@ Rectangle {
|
|||
visible = false;
|
||||
adjustWearablesClosed(status, avatarName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
|
@ -226,7 +230,7 @@ Rectangle {
|
|||
lineHeightMode: Text.FixedHeight
|
||||
lineHeight: 18;
|
||||
text: "Wearable"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
spacing: 10
|
||||
|
@ -237,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: {
|
||||
|
|
|
@ -176,6 +176,7 @@ Item {
|
|||
Item {
|
||||
property alias buttonGlyphText: buttonGlyph.text;
|
||||
property alias buttonText: buttonText.text;
|
||||
property alias glyphSize: buttonGlyph.size;
|
||||
property string buttonColor: hifi.colors.black;
|
||||
property string buttonColor_hover: hifi.colors.blueHighlight;
|
||||
property alias enabled: buttonMouseArea.enabled;
|
||||
|
@ -186,7 +187,8 @@ Item {
|
|||
anchors.top: parent.top;
|
||||
anchors.topMargin: 4;
|
||||
anchors.horizontalCenter: parent.horizontalCenter;
|
||||
anchors.bottom: parent.verticalCenter;
|
||||
anchors.bottom: buttonText.visible ? parent.verticalCenter : parent.bottom;
|
||||
anchors.bottomMargin: buttonText.visible ? 0 : 4;
|
||||
width: parent.width;
|
||||
size: 40;
|
||||
horizontalAlignment: Text.AlignHCenter;
|
||||
|
@ -196,6 +198,7 @@ Item {
|
|||
|
||||
RalewayRegular {
|
||||
id: buttonText;
|
||||
visible: text !== "";
|
||||
anchors.top: parent.verticalCenter;
|
||||
anchors.topMargin: 4;
|
||||
anchors.bottom: parent.bottom;
|
||||
|
@ -300,7 +303,7 @@ Item {
|
|||
anchors.right: certificateButton.left;
|
||||
anchors.top: parent.top;
|
||||
anchors.bottom: parent.bottom;
|
||||
width: 78;
|
||||
width: 72;
|
||||
|
||||
onLoaded: {
|
||||
item.buttonGlyphText = hifi.glyphs.uninstall;
|
||||
|
@ -310,6 +313,10 @@ Item {
|
|||
Commerce.uninstallApp(root.itemHref);
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
trashButton.updateProperties();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
|
@ -319,7 +326,7 @@ Item {
|
|||
anchors.right: uninstallButton.visible ? uninstallButton.left : certificateButton.left;
|
||||
anchors.top: parent.top;
|
||||
anchors.bottom: parent.bottom;
|
||||
width: 84;
|
||||
width: 78;
|
||||
|
||||
onLoaded: {
|
||||
item.buttonGlyphText = hifi.glyphs.update;
|
||||
|
@ -339,6 +346,45 @@ Item {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
trashButton.updateProperties();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: trashButton;
|
||||
visible: root.itemEdition > 0;
|
||||
sourceComponent: contextCardButton;
|
||||
anchors.right: updateButton.visible ? updateButton.left : (uninstallButton.visible ? uninstallButton.left : certificateButton.left);
|
||||
anchors.top: parent.top;
|
||||
anchors.bottom: parent.bottom;
|
||||
width: (updateButton.visible && uninstallButton.visible) ? 15 : 78;
|
||||
|
||||
onLoaded: {
|
||||
item.buttonGlyphText = hifi.glyphs.trash;
|
||||
updateProperties();
|
||||
item.buttonClicked = function() {
|
||||
sendToPurchases({method: 'showTrashLightbox',
|
||||
isInstalled: root.isInstalled,
|
||||
itemHref: root.itemHref,
|
||||
itemName: root.itemName,
|
||||
certID: root.certificateId,
|
||||
itemType: root.itemType,
|
||||
wornEntityID: root.wornEntityID
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateProperties() {
|
||||
if (updateButton.visible && uninstallButton.visible) {
|
||||
item.buttonText = "";
|
||||
item.glyphSize = 20;
|
||||
} else {
|
||||
item.buttonText = "Send to Trash";
|
||||
item.glyphSize = 30;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -651,6 +651,42 @@ Rectangle {
|
|||
lightboxPopup.visible = false;
|
||||
};
|
||||
lightboxPopup.visible = true;
|
||||
} else if (msg.method === "showTrashLightbox") {
|
||||
lightboxPopup.titleText = "Send \"" + msg.itemName + "\" to Trash";
|
||||
lightboxPopup.bodyText = "Sending this item to the Trash means you will no longer own this item " +
|
||||
"and it will be inaccessible to you from Purchases.\n\nThis action cannot be undone.";
|
||||
lightboxPopup.button1text = "CANCEL";
|
||||
lightboxPopup.button1method = function() {
|
||||
lightboxPopup.visible = false;
|
||||
}
|
||||
lightboxPopup.button2text = "CONFIRM";
|
||||
lightboxPopup.button2method = function() {
|
||||
if (msg.isInstalled) {
|
||||
Commerce.uninstallApp(msg.itemHref);
|
||||
}
|
||||
|
||||
if (MyAvatar.skeletonModelURL === msg.itemHref) {
|
||||
MyAvatar.useFullAvatarURL('');
|
||||
}
|
||||
|
||||
if (msg.itemType === "wearable" && msg.wornEntityID !== '') {
|
||||
Entities.deleteEntity(msg.wornEntityID);
|
||||
purchasesModel.setProperty(index, 'wornEntityID', '');
|
||||
}
|
||||
|
||||
Commerce.transferAssetToUsername("trashbot", msg.certID, 1, "Sent " + msg.itemName + " to trash.");
|
||||
|
||||
lightboxPopup.titleText = '"' + msg.itemName + '" Sent to Trash';
|
||||
lightboxPopup.button1text = "OK";
|
||||
lightboxPopup.button1method = function() {
|
||||
root.purchasesReceived = false;
|
||||
lightboxPopup.visible = false;
|
||||
getPurchases();
|
||||
}
|
||||
lightboxPopup.button2text = "";
|
||||
lightboxPopup.bodyText = "";
|
||||
};
|
||||
lightboxPopup.visible = true;
|
||||
} else if (msg.method === "showChangeAvatarLightbox") {
|
||||
lightboxPopup.titleText = "Change Avatar";
|
||||
lightboxPopup.bodyText = "This will change your current avatar to " + msg.itemName + " while retaining your wearables.";
|
||||
|
|
|
@ -189,15 +189,17 @@ Rectangle {
|
|||
var grabbable = MenuInterface.isOptionChecked("Create Entities As Grabbable (except Zones, Particles, and Lights)");
|
||||
|
||||
if (defaultURL.endsWith(".jpg") || defaultURL.endsWith(".png")) {
|
||||
var name = assetProxyModel.data(treeView.selection.currentIndex);
|
||||
var modelURL = "https://hifi-content.s3.amazonaws.com/DomainContent/production/default-image-model.fbx";
|
||||
var textures = JSON.stringify({ "tex.picture": defaultURL});
|
||||
var shapeType = "box";
|
||||
var dynamic = false;
|
||||
var collisionless = true;
|
||||
var position = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getForward(MyAvatar.orientation)));
|
||||
var gravity = Vec3.multiply(Vec3.fromPolar(Math.PI / 2, 0), 0);
|
||||
Entities.addModelEntity(name, modelURL, textures, shapeType, dynamic, collisionless, grabbable, position, gravity);
|
||||
Entities.addEntity({
|
||||
type: "Image",
|
||||
name: assetProxyModel.data(treeView.selection.currentIndex),
|
||||
imageURL: defaultURL,
|
||||
keepAspectRatio: false,
|
||||
dynamic: false,
|
||||
collisionless: true,
|
||||
grabbable: grabbable,
|
||||
position: Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getForward(MyAvatar.orientation))),
|
||||
gravity: Vec3.multiply(Vec3.fromPolar(Math.PI / 2, 0), 0)
|
||||
});
|
||||
} else {
|
||||
var SHAPE_TYPE_NONE = 0;
|
||||
var SHAPE_TYPE_SIMPLE_HULL = 1;
|
||||
|
|
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
|
||||
|
|
|
@ -15,6 +15,7 @@ Item {
|
|||
property var openBrowser: null;
|
||||
property string subMenu: ""
|
||||
signal showDesktop();
|
||||
signal screenChanged(var type, var url);
|
||||
property bool shown: true
|
||||
property int currentApp: -1;
|
||||
property alias tabletApps: tabletApps
|
||||
|
@ -113,6 +114,8 @@ Item {
|
|||
if (loader.item.hasOwnProperty("gotoPreviousApp")) {
|
||||
loader.item.gotoPreviousApp = true;
|
||||
}
|
||||
|
||||
screenChanged("Web", url)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -266,6 +269,24 @@ Item {
|
|||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
var type = "Unknown";
|
||||
if (newSource === "") {
|
||||
type = "Closed";
|
||||
} else if (newSource === "hifi/tablet/TabletMenu.qml") {
|
||||
type = "Menu";
|
||||
} else if (newSource === "hifi/tablet/TabletHome.qml") {
|
||||
type = "Home";
|
||||
} else if (newSource === "hifi/tablet/TabletWebView.qml") {
|
||||
// Handled in `callback()`
|
||||
return;
|
||||
} else if (newSource.toLowerCase().indexOf(".qml") > -1) {
|
||||
type = "QML";
|
||||
} else {
|
||||
console.log("newSource is of unknown type!");
|
||||
}
|
||||
|
||||
screenChanged(type, newSource);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ Windows.ScrollingWindow {
|
|||
id: tabletRoot
|
||||
objectName: "tabletRoot"
|
||||
property string username: "Unknown user"
|
||||
signal screenChanged(var type, var url);
|
||||
|
||||
property var rootMenu;
|
||||
property string subMenu: ""
|
||||
|
@ -69,6 +70,8 @@ Windows.ScrollingWindow {
|
|||
if (loader.item.hasOwnProperty("closeButtonVisible")) {
|
||||
loader.item.closeButtonVisible = false;
|
||||
}
|
||||
|
||||
screenChanged("Web", url);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -179,7 +182,25 @@ Windows.ScrollingWindow {
|
|||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
var type = "Unknown";
|
||||
if (newSource === "") {
|
||||
type = "Closed";
|
||||
} else if (newSource === "hifi/tablet/TabletMenu.qml") {
|
||||
type = "Menu";
|
||||
} else if (newSource === "hifi/tablet/TabletHome.qml") {
|
||||
type = "Home";
|
||||
} else if (newSource === "hifi/tablet/TabletWebView.qml") {
|
||||
// Handled in `callback()`
|
||||
return;
|
||||
} else if (newSource.toLowerCase().indexOf(".qml") > -1) {
|
||||
type = "QML";
|
||||
} else {
|
||||
console.log("newSource is of unknown type!");
|
||||
}
|
||||
|
||||
screenChanged(type, newSource);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,6 +159,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"
|
||||
|
@ -171,6 +172,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"
|
||||
|
@ -209,6 +211,8 @@
|
|||
#include "InterfaceParentFinder.h"
|
||||
#include "ui/OctreeStatsProvider.h"
|
||||
|
||||
#include "avatar/GrabManager.h"
|
||||
|
||||
#include <GPUIdent.h>
|
||||
#include <gl/GLHelpers.h>
|
||||
#include <src/scripting/GooglePolyScriptingInterface.h>
|
||||
|
@ -721,6 +725,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) {
|
||||
|
@ -859,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);
|
||||
|
@ -919,6 +924,8 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
|
|||
DependencyManager::set<ResourceRequestObserver>();
|
||||
DependencyManager::set<Keyboard>();
|
||||
DependencyManager::set<KeyboardScriptingInterface>();
|
||||
DependencyManager::set<GrabManager>();
|
||||
DependencyManager::set<AvatarPackager>();
|
||||
|
||||
return previousSessionCrashed;
|
||||
}
|
||||
|
@ -969,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)),
|
||||
|
@ -1058,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");
|
||||
|
@ -1078,6 +1083,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
nodeList->startThread();
|
||||
nodeList->setFlagTimeForConnectionStep(true);
|
||||
|
||||
// move the AddressManager to the NodeList thread so that domain resets due to domain changes always occur
|
||||
// before we tell MyAvatar to go to a new location in the new domain
|
||||
|
@ -1573,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);
|
||||
|
@ -2045,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);
|
||||
|
@ -2064,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();
|
||||
|
||||
|
@ -2289,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);
|
||||
|
@ -2315,6 +2312,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
DependencyManager::get<PickManager>()->setPrecisionPicking(rayPickID, value);
|
||||
});
|
||||
|
||||
EntityTreeRenderer::setGetAvatarUpOperator([] {
|
||||
return DependencyManager::get<AvatarManager>()->getMyAvatar()->getWorldOrientation() * Vectors::UP;
|
||||
});
|
||||
|
||||
// Preload Tablet sounds
|
||||
DependencyManager::get<TabletScriptingInterface>()->preloadSounds();
|
||||
DependencyManager::get<Keyboard>()->createKeyboard();
|
||||
|
@ -2458,8 +2459,15 @@ void Application::updateHeartbeat() const {
|
|||
}
|
||||
|
||||
void Application::onAboutToQuit() {
|
||||
// quickly save AvatarEntityData before the EntityTree is dismantled
|
||||
getMyAvatar()->saveAvatarEntityDataToSettings();
|
||||
|
||||
emit beforeAboutToQuit();
|
||||
|
||||
if (getLoginDialogPoppedUp() && _firstRun.get()) {
|
||||
_firstRun.set(false);
|
||||
}
|
||||
|
||||
foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) {
|
||||
if (inputPlugin->isActive()) {
|
||||
inputPlugin->deactivate();
|
||||
|
@ -2614,6 +2622,7 @@ void Application::cleanupBeforeQuit() {
|
|||
DependencyManager::destroy<PickManager>();
|
||||
DependencyManager::destroy<KeyboardScriptingInterface>();
|
||||
DependencyManager::destroy<Keyboard>();
|
||||
DependencyManager::destroy<AvatarPackager>();
|
||||
|
||||
qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete";
|
||||
}
|
||||
|
@ -4908,7 +4917,7 @@ void Application::calibrateEyeTracker5Points() {
|
|||
#endif
|
||||
|
||||
bool Application::exportEntities(const QString& filename,
|
||||
const QVector<EntityItemID>& entityIDs,
|
||||
const QVector<QUuid>& entityIDs,
|
||||
const glm::vec3* givenOffset) {
|
||||
QHash<EntityItemID, EntityItemPointer> entities;
|
||||
|
||||
|
@ -4983,16 +4992,12 @@ bool Application::exportEntities(const QString& filename, float x, float y, floa
|
|||
glm::vec3 minCorner = center - vec3(scale);
|
||||
float cubeSize = scale * 2;
|
||||
AACube boundingCube(minCorner, cubeSize);
|
||||
QVector<EntityItemPointer> entities;
|
||||
QVector<EntityItemID> ids;
|
||||
QVector<QUuid> entities;
|
||||
auto entityTree = getEntities()->getTree();
|
||||
entityTree->withReadLock([&] {
|
||||
entityTree->findEntities(boundingCube, entities);
|
||||
foreach(EntityItemPointer entity, entities) {
|
||||
ids << entity->getEntityItemID();
|
||||
}
|
||||
entityTree->evalEntitiesInCube(boundingCube, PickFilter(), entities);
|
||||
});
|
||||
return exportEntities(filename, ids, ¢er);
|
||||
return exportEntities(filename, entities, ¢er);
|
||||
}
|
||||
|
||||
void Application::loadSettings() {
|
||||
|
@ -5257,7 +5262,8 @@ void Application::resumeAfterLoginDialogActionTaken() {
|
|||
// this will force the model the look at the correct directory (weird order of operations issue)
|
||||
scriptEngines->reloadLocalFiles();
|
||||
|
||||
if (!_defaultScriptsLocation.exists()) {
|
||||
// if the --scripts command-line argument was used.
|
||||
if (!_defaultScriptsLocation.exists() && (arguments().indexOf(QString("--").append(SCRIPTS_SWITCH))) != -1) {
|
||||
scriptEngines->loadDefaultScripts();
|
||||
scriptEngines->defaultScriptsLocationOverridden(true);
|
||||
} else {
|
||||
|
@ -5265,39 +5271,25 @@ void Application::resumeAfterLoginDialogActionTaken() {
|
|||
}
|
||||
}
|
||||
|
||||
if (_firstRun.get()) {
|
||||
// not first run anymore since action was taken.
|
||||
_firstRun.set(false);
|
||||
}
|
||||
|
||||
auto accountManager = DependencyManager::get<AccountManager>();
|
||||
auto addressManager = DependencyManager::get<AddressManager>();
|
||||
|
||||
// restart domain handler.
|
||||
nodeList->getDomainHandler().resetting();
|
||||
|
||||
if (!accountManager->isLoggedIn()) {
|
||||
QVariant testProperty = property(hifi::properties::TEST);
|
||||
if (testProperty.isValid()) {
|
||||
const auto testScript = property(hifi::properties::TEST).toUrl();
|
||||
// Set last parameter to exit interface when the test script finishes, if so requested
|
||||
DependencyManager::get<ScriptEngines>()->loadScript(testScript, false, false, false, false, quitWhenFinished);
|
||||
// This is done so we don't get a "connection time-out" message when we haven't passed in a URL.
|
||||
if (arguments().contains("--url")) {
|
||||
auto reply = SandboxUtils::getStatus();
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); });
|
||||
} else {
|
||||
addressManager->goToEntry();
|
||||
}
|
||||
} else {
|
||||
QVariant testProperty = property(hifi::properties::TEST);
|
||||
if (testProperty.isValid()) {
|
||||
const auto testScript = property(hifi::properties::TEST).toUrl();
|
||||
// Set last parameter to exit interface when the test script finishes, if so requested
|
||||
DependencyManager::get<ScriptEngines>()->loadScript(testScript, false, false, false, false, quitWhenFinished);
|
||||
// This is done so we don't get a "connection time-out" message when we haven't passed in a URL.
|
||||
if (arguments().contains("--url")) {
|
||||
auto reply = SandboxUtils::getStatus();
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); });
|
||||
}
|
||||
} else {
|
||||
auto reply = SandboxUtils::getStatus();
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); });
|
||||
}
|
||||
auto reply = SandboxUtils::getStatus();
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); });
|
||||
}
|
||||
|
||||
auto menu = Menu::getInstance();
|
||||
|
@ -5980,6 +5972,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));
|
||||
}
|
||||
}
|
||||
|
@ -6098,6 +6092,9 @@ void Application::update(float deltaTime) {
|
|||
updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process...
|
||||
updateDialogs(deltaTime); // update various stats dialogs if present
|
||||
|
||||
auto grabManager = DependencyManager::get<GrabManager>();
|
||||
grabManager->simulateGrabs();
|
||||
|
||||
QSharedPointer<AvatarManager> avatarManager = DependencyManager::get<AvatarManager>();
|
||||
|
||||
{
|
||||
|
@ -6701,6 +6698,7 @@ void Application::resetSensors(bool andReload) {
|
|||
DependencyManager::get<DdeFaceTracker>()->reset();
|
||||
DependencyManager::get<EyeTracker>()->reset();
|
||||
_overlayConductor.centerUI();
|
||||
getActiveDisplayPlugin()->resetSensors();
|
||||
getMyAvatar()->reset(true, andReload);
|
||||
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(), "reset", Qt::QueuedConnection);
|
||||
}
|
||||
|
@ -6765,8 +6763,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;
|
||||
}
|
||||
|
@ -6794,8 +6794,6 @@ void Application::clearDomainOctreeDetails() {
|
|||
ShaderCache::instance().clearUnusedResources();
|
||||
DependencyManager::get<TextureCache>()->clearUnusedResources();
|
||||
DependencyManager::get<recording::ClipCache>()->clearUnusedResources();
|
||||
|
||||
getMyAvatar()->setAvatarEntityDataChanged(true);
|
||||
}
|
||||
|
||||
void Application::domainURLChanged(QUrl domainURL) {
|
||||
|
@ -7004,6 +7002,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
|
||||
|
@ -7703,16 +7702,13 @@ void Application::addAssetToWorldSetMapping(QString filePath, QString mapping, Q
|
|||
|
||||
void Application::addAssetToWorldAddEntity(QString filePath, QString mapping) {
|
||||
EntityItemProperties properties;
|
||||
properties.setType(EntityTypes::Model);
|
||||
properties.setName(mapping.right(mapping.length() - 1));
|
||||
if (filePath.toLower().endsWith(PNG_EXTENSION) || filePath.toLower().endsWith(JPG_EXTENSION)) {
|
||||
QJsonObject textures {
|
||||
{"tex.picture", QString("atp:" + mapping) }
|
||||
};
|
||||
properties.setModelURL("https://hifi-content.s3.amazonaws.com/DomainContent/production/default-image-model.fbx");
|
||||
properties.setTextures(QJsonDocument(textures).toJson(QJsonDocument::Compact));
|
||||
properties.setShapeType(SHAPE_TYPE_BOX);
|
||||
properties.setType(EntityTypes::Image);
|
||||
properties.setImageURL(QString("atp:" + mapping));
|
||||
properties.setKeepAspectRatio(false);
|
||||
} else {
|
||||
properties.setType(EntityTypes::Model);
|
||||
properties.setModelURL("atp:" + mapping);
|
||||
properties.setShapeType(SHAPE_TYPE_SIMPLE_COMPOUND);
|
||||
}
|
||||
|
@ -8724,6 +8720,14 @@ void Application::updateLoginDialogOverlayPosition() {
|
|||
}
|
||||
}
|
||||
|
||||
bool Application::hasRiftControllers() {
|
||||
return PluginUtils::isOculusTouchControllerAvailable();
|
||||
}
|
||||
|
||||
bool Application::hasViveControllers() {
|
||||
return PluginUtils::isViveControllerAvailable();
|
||||
}
|
||||
|
||||
void Application::onDismissedLoginDialog() {
|
||||
_loginDialogPoppedUp = false;
|
||||
loginDialogPoppedUp.set(false);
|
||||
|
@ -8942,6 +8946,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>();
|
||||
|
|
|
@ -120,7 +120,7 @@ class Application : public QApplication,
|
|||
public:
|
||||
// virtual functions required for PluginContainer
|
||||
virtual ui::Menu* getPrimaryMenu() override;
|
||||
virtual void requestReset() override { resetSensors(true); }
|
||||
virtual void requestReset() override { resetSensors(false); }
|
||||
virtual void showDisplayPluginsTools(bool show) override;
|
||||
virtual GLWidget* getPrimaryWidget() override;
|
||||
virtual MainWindow* getPrimaryWindow() override;
|
||||
|
@ -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();
|
||||
|
@ -351,7 +355,7 @@ signals:
|
|||
|
||||
public slots:
|
||||
QVector<EntityItemID> pasteEntities(float x, float y, float z);
|
||||
bool exportEntities(const QString& filename, const QVector<EntityItemID>& entityIDs, const glm::vec3* givenOffset = nullptr);
|
||||
bool exportEntities(const QString& filename, const QVector<QUuid>& entityIDs, const glm::vec3* givenOffset = nullptr);
|
||||
bool exportEntities(const QString& filename, float x, float y, float z, float scale);
|
||||
bool importEntities(const QString& url, const bool isObservable = true, const qint64 callerId = -1);
|
||||
void updateThreadPoolCount() const;
|
||||
|
@ -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"
|
||||
|
@ -48,6 +49,7 @@
|
|||
#include "DeferredLightingEffect.h"
|
||||
#include "PickManager.h"
|
||||
|
||||
#include "LightingModel.h"
|
||||
#include "AmbientOcclusionEffect.h"
|
||||
#include "RenderShadowTask.h"
|
||||
#include "AntialiasingEffect.h"
|
||||
|
@ -143,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()));
|
||||
|
@ -393,13 +399,9 @@ Menu::Menu() {
|
|||
connect(action, &QAction::triggered, [action] {
|
||||
auto renderConfig = qApp->getRenderEngine()->getConfiguration();
|
||||
if (renderConfig) {
|
||||
auto mainViewShadowTaskConfig = renderConfig->getConfig<RenderShadowTask>("RenderMainView.RenderShadowTask");
|
||||
if (mainViewShadowTaskConfig) {
|
||||
if (action->isChecked()) {
|
||||
mainViewShadowTaskConfig->setPreset("Enabled");
|
||||
} else {
|
||||
mainViewShadowTaskConfig->setPreset("None");
|
||||
}
|
||||
auto lightingModelConfig = renderConfig->getConfig<MakeLightingModel>("RenderMainView.LightingModel");
|
||||
if (lightingModelConfig) {
|
||||
lightingModelConfig->setShadow(action->isChecked());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -408,15 +410,11 @@ Menu::Menu() {
|
|||
connect(action, &QAction::triggered, [action] {
|
||||
auto renderConfig = qApp->getRenderEngine()->getConfiguration();
|
||||
if (renderConfig) {
|
||||
auto mainViewAmbientOcclusionConfig = renderConfig->getConfig<AmbientOcclusionEffect>("RenderMainView.AmbientOcclusion");
|
||||
if (mainViewAmbientOcclusionConfig) {
|
||||
if (action->isChecked()) {
|
||||
mainViewAmbientOcclusionConfig->setPreset("Enabled");
|
||||
} else {
|
||||
mainViewAmbientOcclusionConfig->setPreset("None");
|
||||
}
|
||||
auto lightingModelConfig = renderConfig->getConfig<MakeLightingModel>("RenderMainView.LightingModel");
|
||||
if (lightingModelConfig) {
|
||||
lightingModelConfig->setAmbientOcclusion(action->isChecked());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::WorldAxes);
|
||||
|
@ -652,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";
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
#include <RenderDeferredTask.h>
|
||||
#include <RenderForwardTask.h>
|
||||
#include <RenderViewTask.h>
|
||||
|
||||
#include <glm/gtx/transform.hpp>
|
||||
#include <gpu/Context.h>
|
||||
|
@ -270,14 +271,8 @@ public:
|
|||
|
||||
void SecondaryCameraRenderTask::build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor, bool isDeferred) {
|
||||
const auto cachedArg = task.addJob<SecondaryCameraJob>("SecondaryCamera");
|
||||
const auto items = task.addJob<RenderFetchCullSortTask>("FetchCullSort", cullFunctor, render::ItemKey::TAG_BITS_1, render::ItemKey::TAG_BITS_1);
|
||||
assert(items.canCast<RenderFetchCullSortTask::Output>());
|
||||
if (isDeferred) {
|
||||
const render::Varying cascadeSceneBBoxes;
|
||||
const auto renderInput = RenderDeferredTask::Input(items, cascadeSceneBBoxes).asVarying();
|
||||
task.addJob<RenderDeferredTask>("RenderDeferredTask", renderInput, false);
|
||||
} else {
|
||||
task.addJob<RenderForwardTask>("Forward", items);
|
||||
}
|
||||
|
||||
task.addJob<RenderViewTask>("RenderSecondView", cullFunctor, isDeferred, render::ItemKey::TAG_BITS_1, render::ItemKey::TAG_BITS_1);
|
||||
|
||||
task.addJob<EndSecondaryCameraFrame>("EndSecondaryCamera", cachedArg);
|
||||
}
|
|
@ -58,6 +58,16 @@ AvatarActionHold::~AvatarActionHold() {
|
|||
#endif
|
||||
}
|
||||
|
||||
void AvatarActionHold::removeFromOwner() {
|
||||
auto avatarManager = DependencyManager::get<AvatarManager>();
|
||||
if (avatarManager) {
|
||||
auto myAvatar = avatarManager->getMyAvatar();
|
||||
if (myAvatar) {
|
||||
myAvatar->removeHoldAction(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool AvatarActionHold::getAvatarRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation) {
|
||||
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
|
||||
MyCharacterController* controller = myAvatar ? myAvatar->getCharacterController() : nullptr;
|
||||
|
@ -143,7 +153,7 @@ bool AvatarActionHold::getTarget(float deltaTimeStep, glm::quat& rotation, glm::
|
|||
ownerEntity->setTransitingWithAvatar(_isTransitingWithAvatar);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (holdingAvatar->isMyAvatar()) {
|
||||
std::shared_ptr<MyAvatar> myAvatar = avatarManager->getMyAvatar();
|
||||
|
||||
|
@ -226,7 +236,7 @@ bool AvatarActionHold::getTarget(float deltaTimeStep, glm::quat& rotation, glm::
|
|||
}
|
||||
|
||||
rotation = palmRotation * _relativeRotation;
|
||||
position = palmPosition + rotation * _relativePosition;
|
||||
position = palmPosition + palmRotation * _relativePosition;
|
||||
|
||||
// update linearVelocity based on offset via _relativePosition;
|
||||
linearVelocity = linearVelocity + glm::cross(angularVelocity, position - palmPosition);
|
||||
|
@ -369,8 +379,12 @@ bool AvatarActionHold::updateArguments(QVariantMap arguments) {
|
|||
hand = _hand;
|
||||
}
|
||||
|
||||
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
|
||||
holderID = myAvatar->getSessionUUID();
|
||||
ok = true;
|
||||
holderID = EntityDynamicInterface::extractStringArgument("hold", arguments, "holderID", ok, false);
|
||||
if (!ok) {
|
||||
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
|
||||
holderID = myAvatar->getSessionUUID();
|
||||
}
|
||||
|
||||
ok = true;
|
||||
kinematic = EntityDynamicInterface::extractBooleanArgument("hold", arguments, "kinematic", ok, false);
|
||||
|
@ -417,13 +431,13 @@ bool AvatarActionHold::updateArguments(QVariantMap arguments) {
|
|||
_kinematicSetVelocity = kinematicSetVelocity;
|
||||
_ignoreIK = ignoreIK;
|
||||
_active = true;
|
||||
|
||||
|
||||
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
|
||||
|
||||
auto ownerEntity = _ownerEntity.lock();
|
||||
if (ownerEntity) {
|
||||
ownerEntity->setDynamicDataDirty(true);
|
||||
ownerEntity->setDynamicDataNeedsTransmit(true);
|
||||
ownerEntity->setDynamicDataNeedsTransmit(true);
|
||||
ownerEntity->setTransitingWithAvatar(myAvatar->getTransit()->isActive());
|
||||
}
|
||||
});
|
||||
|
|
|
@ -26,6 +26,8 @@ public:
|
|||
AvatarActionHold(const QUuid& id, EntityItemPointer ownerEntity);
|
||||
virtual ~AvatarActionHold();
|
||||
|
||||
virtual void removeFromOwner() override;
|
||||
|
||||
virtual bool updateArguments(QVariantMap arguments) override;
|
||||
virtual QVariantMap getArguments() override;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -887,3 +892,13 @@ QVariantMap AvatarManager::getPalData(const QStringList& specificAvatarIdentifie
|
|||
doc.insert("data", palData);
|
||||
return doc.toVariantMap();
|
||||
}
|
||||
|
||||
void AvatarManager::accumulateGrabPositions(std::map<QUuid, GrabLocationAccumulator>& grabAccumulators) {
|
||||
auto avatarMap = getHashCopy();
|
||||
AvatarHash::iterator itr = avatarMap.begin();
|
||||
while (itr != avatarMap.end()) {
|
||||
const auto& avatar = std::static_pointer_cast<Avatar>(*itr);
|
||||
avatar->accumulateGrabPositions(grabAccumulators);
|
||||
itr++;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -198,6 +198,8 @@ public:
|
|||
void handleProcessedPhysicsTransaction(PhysicsEngine::Transaction& transaction);
|
||||
void removeDeadAvatarEntities(const SetOfEntities& deadEntities);
|
||||
|
||||
void accumulateGrabPositions(std::map<QUuid, GrabLocationAccumulator>& grabAccumulators);
|
||||
|
||||
public slots:
|
||||
/**jsdoc
|
||||
* @function AvatarManager.updateAvatarRenderStatus
|
||||
|
@ -215,7 +217,7 @@ private:
|
|||
void simulateAvatarFades(float deltaTime);
|
||||
|
||||
AvatarSharedPointer newSharedAvatar() override;
|
||||
|
||||
|
||||
// called only from the AvatarHashMap thread - cannot be called while this thread holds the
|
||||
// hash lock, since handleRemovedAvatar needs a write lock on the entity tree and the entity tree
|
||||
// frequently grabs a read lock on the hash to get a given avatar by ID
|
||||
|
|
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
39
interface/src/avatar/GrabManager.cpp
Normal file
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// GrabManager.cpp
|
||||
// interface/src/avatar/
|
||||
//
|
||||
// Created by Seth Alves on 2018-12-4.
|
||||
// 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 "GrabManager.h"
|
||||
|
||||
void GrabManager::simulateGrabs() {
|
||||
QSharedPointer<AvatarManager> avatarManager = DependencyManager::get<AvatarManager>();
|
||||
|
||||
// Update grabbed objects
|
||||
auto entityTreeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||
auto entityTree = entityTreeRenderer->getTree();
|
||||
entityTree->withReadLock([&] {
|
||||
PROFILE_RANGE(simulation, "Grabs");
|
||||
|
||||
std::map<QUuid, GrabLocationAccumulator> grabAccumulators;
|
||||
avatarManager->accumulateGrabPositions(grabAccumulators);
|
||||
|
||||
for (auto& accumulatedLocation : grabAccumulators) {
|
||||
QUuid grabbedThingID = accumulatedLocation.first;
|
||||
GrabLocationAccumulator& acc = accumulatedLocation.second;
|
||||
bool success;
|
||||
SpatiallyNestablePointer grabbedThing = SpatiallyNestable::findByID(grabbedThingID, success);
|
||||
if (success && grabbedThing) {
|
||||
glm::vec3 finalPosition = acc.finalizePosition();
|
||||
glm::quat finalOrientation = acc.finalizeOrientation();
|
||||
grabbedThing->setTransform(createMatFromQuatAndPos(finalOrientation, finalPosition));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
23
interface/src/avatar/GrabManager.h
Normal file
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// GrabManager.h
|
||||
// interface/src/avatar/
|
||||
//
|
||||
// Created by Seth Alves on 2018-12-4.
|
||||
// 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 <AvatarData.h>
|
||||
#include <EntityTreeRenderer.h>
|
||||
#include "AvatarManager.h"
|
||||
|
||||
class GrabManager : public QObject, public Dependency {
|
||||
Q_OBJECT
|
||||
SINGLETON_DEPENDENCY
|
||||
|
||||
public:
|
||||
void simulateGrabs();
|
||||
|
||||
};
|
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
|
@ -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
|
|
@ -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,31 @@ public:
|
|||
virtual void setAttachmentsVariant(const QVariantList& variant) override;
|
||||
|
||||
glm::vec3 getNextPosition() { return _goToPending ? _goToPosition : getWorldPosition(); }
|
||||
void prepareAvatarEntityDataForReload();
|
||||
|
||||
/**jsdoc
|
||||
* Create a new grab.
|
||||
* @function MyAvatar.grab
|
||||
* @param {Uuid} targetID - id of grabbed thing
|
||||
* @param {number} parentJointIndex - avatar joint being used to grab
|
||||
* @param {Vec3} offset - target's positional offset from joint
|
||||
* @param {Quat} rotationalOffset - target's rotational offset from joint
|
||||
* @returns {Uuid} id of the new grab
|
||||
*/
|
||||
Q_INVOKABLE const QUuid grab(const QUuid& targetID, int parentJointIndex,
|
||||
glm::vec3 positionalOffset, glm::quat rotationalOffset);
|
||||
|
||||
/**jsdoc
|
||||
* Release (delete) a grab.
|
||||
* @function MyAvatar.releaseGrab
|
||||
* @param {Uuid} grabID - id of grabbed thing
|
||||
*/
|
||||
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:
|
||||
|
||||
|
@ -1368,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
|
||||
|
@ -1470,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
|
||||
|
@ -1559,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;
|
||||
|
@ -1878,6 +1935,7 @@ private:
|
|||
bool _haveReceivedHeightLimitsFromDomain { false };
|
||||
int _disableHandTouchCount { 0 };
|
||||
bool _skeletonModelLoaded { false };
|
||||
bool _reloadAvatarEntityDataFromSettings { true };
|
||||
|
||||
Setting::Handle<QString> _dominantHandSetting;
|
||||
Setting::Handle<float> _headPitchSetting;
|
||||
|
@ -1896,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);
|
||||
|
|
|
@ -301,8 +301,8 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
|||
eyeParams.eyeSaccade = head->getSaccade();
|
||||
eyeParams.modelRotation = getRotation();
|
||||
eyeParams.modelTranslation = getTranslation();
|
||||
eyeParams.leftEyeJointIndex = hfmModel.leftEyeJointIndex;
|
||||
eyeParams.rightEyeJointIndex = hfmModel.rightEyeJointIndex;
|
||||
eyeParams.leftEyeJointIndex = _rig.indexOfJoint("LeftEye");
|
||||
eyeParams.rightEyeJointIndex = _rig.indexOfJoint("RightEye");
|
||||
|
||||
_rig.updateFromEyeParameters(eyeParams);
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -125,7 +125,7 @@ LaserPointer::RenderState::RenderState(const OverlayID& startID, const OverlayID
|
|||
StartEndRenderState(startID, endID), _pathID(pathID)
|
||||
{
|
||||
if (!_pathID.isNull()) {
|
||||
_pathIgnoreRays = qApp->getOverlays().getProperty(_pathID, "ignoreRayIntersection").value.toBool();
|
||||
_pathIgnoreRays = qApp->getOverlays().getProperty(_pathID, "ignorePickIntersection").value.toBool();
|
||||
_lineWidth = qApp->getOverlays().getProperty(_pathID, "lineWidth").value.toFloat();
|
||||
}
|
||||
}
|
||||
|
@ -142,7 +142,7 @@ void LaserPointer::RenderState::disable() {
|
|||
if (!getPathID().isNull()) {
|
||||
QVariantMap pathProps;
|
||||
pathProps.insert("visible", false);
|
||||
pathProps.insert("ignoreRayIntersection", true);
|
||||
pathProps.insert("ignorePickIntersection", true);
|
||||
qApp->getOverlays().editOverlay(getPathID(), pathProps);
|
||||
}
|
||||
}
|
||||
|
@ -156,7 +156,7 @@ void LaserPointer::RenderState::update(const glm::vec3& origin, const glm::vec3&
|
|||
pathProps.insert("start", vec3toVariant(origin));
|
||||
pathProps.insert("end", endVariant);
|
||||
pathProps.insert("visible", true);
|
||||
pathProps.insert("ignoreRayIntersection", doesPathIgnoreRays());
|
||||
pathProps.insert("ignorePickIntersection", doesPathIgnoreRays());
|
||||
pathProps.insert("lineWidth", getLineWidth() * parentScale);
|
||||
qApp->getOverlays().editOverlay(getPathID(), pathProps);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ class LaserPointerScriptingInterface : public QObject, public Dependency {
|
|||
SINGLETON_DEPENDENCY
|
||||
|
||||
/**jsdoc
|
||||
* Synonym for {@link Pointers} as used for laser pointers.
|
||||
* Synonym for {@link Pointers} as used for laser pointers. Deprecated.
|
||||
*
|
||||
* @namespace LaserPointers
|
||||
*
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
#include "Application.h"
|
||||
#include "EntityScriptingInterface.h"
|
||||
#include "PickScriptingInterface.h"
|
||||
#include "ui/overlays/Overlays.h"
|
||||
#include "avatar/AvatarManager.h"
|
||||
#include "scripting/HMDScriptingInterface.h"
|
||||
|
@ -57,10 +58,15 @@ PickParabola ParabolaPick::getMathematicalPick() const {
|
|||
|
||||
PickResultPointer ParabolaPick::getEntityIntersection(const PickParabola& pick) {
|
||||
if (glm::length2(pick.acceleration) > EPSILON && glm::length2(pick.velocity) > EPSILON) {
|
||||
bool precisionPicking = !(getFilter().doesPickCoarse() || DependencyManager::get<PickManager>()->getForceCoarsePicking());
|
||||
PickFilter searchFilter = getFilter();
|
||||
if (DependencyManager::get<PickManager>()->getForceCoarsePicking()) {
|
||||
searchFilter.setFlag(PickFilter::COARSE, true);
|
||||
searchFilter.setFlag(PickFilter::PRECISE, false);
|
||||
}
|
||||
|
||||
ParabolaToEntityIntersectionResult entityRes =
|
||||
DependencyManager::get<EntityScriptingInterface>()->findParabolaIntersectionVector(pick, precisionPicking,
|
||||
getIncludeItemsAs<EntityItemID>(), getIgnoreItemsAs<EntityItemID>(), !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable());
|
||||
DependencyManager::get<EntityScriptingInterface>()->evalParabolaIntersectionVector(pick, searchFilter,
|
||||
getIncludeItemsAs<EntityItemID>(), getIgnoreItemsAs<EntityItemID>());
|
||||
if (entityRes.intersects) {
|
||||
return std::make_shared<ParabolaPickResult>(IntersectionType::ENTITY, entityRes.entityID, entityRes.distance, entityRes.parabolicDistance, entityRes.intersection, pick, entityRes.surfaceNormal, entityRes.extraInfo);
|
||||
}
|
||||
|
@ -70,7 +76,7 @@ PickResultPointer ParabolaPick::getEntityIntersection(const PickParabola& pick)
|
|||
|
||||
PickResultPointer ParabolaPick::getOverlayIntersection(const PickParabola& pick) {
|
||||
if (glm::length2(pick.acceleration) > EPSILON && glm::length2(pick.velocity) > EPSILON) {
|
||||
bool precisionPicking = !(getFilter().doesPickCoarse() || DependencyManager::get<PickManager>()->getForceCoarsePicking());
|
||||
bool precisionPicking = !(getFilter().isCoarse() || DependencyManager::get<PickManager>()->getForceCoarsePicking());
|
||||
ParabolaToOverlayIntersectionResult overlayRes =
|
||||
qApp->getOverlays().findParabolaIntersectionVector(pick, precisionPicking,
|
||||
getIncludeItemsAs<OverlayID>(), getIgnoreItemsAs<OverlayID>(), !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable());
|
||||
|
|
|
@ -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()) {
|
||||
|
@ -253,12 +253,12 @@ StartEndRenderState::StartEndRenderState(const OverlayID& startID, const Overlay
|
|||
_startID(startID), _endID(endID) {
|
||||
if (!_startID.isNull()) {
|
||||
_startDim = vec3FromVariant(qApp->getOverlays().getProperty(_startID, "dimensions").value);
|
||||
_startIgnoreRays = qApp->getOverlays().getProperty(_startID, "ignoreRayIntersection").value.toBool();
|
||||
_startIgnoreRays = qApp->getOverlays().getProperty(_startID, "ignorePickIntersection").value.toBool();
|
||||
}
|
||||
if (!_endID.isNull()) {
|
||||
_endDim = vec3FromVariant(qApp->getOverlays().getProperty(_endID, "dimensions").value);
|
||||
_endRot = quatFromVariant(qApp->getOverlays().getProperty(_endID, "rotation").value);
|
||||
_endIgnoreRays = qApp->getOverlays().getProperty(_endID, "ignoreRayIntersection").value.toBool();
|
||||
_endIgnoreRays = qApp->getOverlays().getProperty(_endID, "ignorePickIntersection").value.toBool();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -275,13 +275,13 @@ void StartEndRenderState::disable() {
|
|||
if (!getStartID().isNull()) {
|
||||
QVariantMap startProps;
|
||||
startProps.insert("visible", false);
|
||||
startProps.insert("ignoreRayIntersection", true);
|
||||
startProps.insert("ignorePickIntersection", true);
|
||||
qApp->getOverlays().editOverlay(getStartID(), startProps);
|
||||
}
|
||||
if (!getEndID().isNull()) {
|
||||
QVariantMap endProps;
|
||||
endProps.insert("visible", false);
|
||||
endProps.insert("ignoreRayIntersection", true);
|
||||
endProps.insert("ignorePickIntersection", true);
|
||||
qApp->getOverlays().editOverlay(getEndID(), endProps);
|
||||
}
|
||||
_enabled = false;
|
||||
|
@ -294,7 +294,7 @@ void StartEndRenderState::update(const glm::vec3& origin, const glm::vec3& end,
|
|||
startProps.insert("position", vec3toVariant(origin));
|
||||
startProps.insert("visible", true);
|
||||
startProps.insert("dimensions", vec3toVariant(getStartDim() * parentScale));
|
||||
startProps.insert("ignoreRayIntersection", doesStartIgnoreRays());
|
||||
startProps.insert("ignorePickIntersection", doesStartIgnoreRays());
|
||||
qApp->getOverlays().editOverlay(getStartID(), startProps);
|
||||
}
|
||||
|
||||
|
@ -346,7 +346,7 @@ void StartEndRenderState::update(const glm::vec3& origin, const glm::vec3& end,
|
|||
endProps.insert("position", vec3toVariant(position));
|
||||
endProps.insert("rotation", quatToVariant(rotation));
|
||||
endProps.insert("visible", true);
|
||||
endProps.insert("ignoreRayIntersection", doesEndIgnoreRays());
|
||||
endProps.insert("ignorePickIntersection", doesEndIgnoreRays());
|
||||
qApp->getOverlays().editOverlay(getEndID(), endProps);
|
||||
}
|
||||
_enabled = true;
|
||||
|
|
|
@ -49,11 +49,17 @@ unsigned int PickScriptingInterface::createPick(const PickQuery::PickType type,
|
|||
}
|
||||
}
|
||||
|
||||
PickFilter getPickFilter(unsigned int filter) {
|
||||
// FIXME: Picks always intersect visible and collidable things right now
|
||||
filter = filter | (PickScriptingInterface::PICK_INCLUDE_VISIBLE() | PickScriptingInterface::PICK_INCLUDE_COLLIDABLE());
|
||||
return PickFilter(filter);
|
||||
}
|
||||
|
||||
/**jsdoc
|
||||
* A set of properties that can be passed to {@link Picks.createPick} to create a new Ray Pick.
|
||||
* @typedef {object} Picks.RayPickProperties
|
||||
* @property {boolean} [enabled=false] If this Pick should start enabled or not. Disabled Picks do not updated their pick results.
|
||||
* @property {number} [filter=Picks.PICK_NOTHING] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
|
||||
* @property {number} [filter=0] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
|
||||
* @property {number} [maxDistance=0.0] The max distance at which this Pick will intersect. 0.0 = no max. < 0.0 is invalid.
|
||||
* @property {Uuid} parentID - The ID of the parent, either an avatar, an entity, an overlay, or a pick.
|
||||
* @property {number} [parentJointIndex=0] - The joint of the parent to parent to, for example, the joints on the model of an avatar. (default = 0, no joint)
|
||||
|
@ -73,7 +79,7 @@ unsigned int PickScriptingInterface::createRayPick(const QVariant& properties) {
|
|||
|
||||
PickFilter filter = PickFilter();
|
||||
if (propMap["filter"].isValid()) {
|
||||
filter = PickFilter(propMap["filter"].toUInt());
|
||||
filter = getPickFilter(propMap["filter"].toUInt());
|
||||
}
|
||||
|
||||
float maxDistance = 0.0f;
|
||||
|
@ -111,7 +117,7 @@ unsigned int PickScriptingInterface::createRayPick(const QVariant& properties) {
|
|||
* @typedef {object} Picks.StylusPickProperties
|
||||
* @property {number} [hand=-1] An integer. 0 == left, 1 == right. Invalid otherwise.
|
||||
* @property {boolean} [enabled=false] If this Pick should start enabled or not. Disabled Picks do not updated their pick results.
|
||||
* @property {number} [filter=Picks.PICK_NOTHING] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
|
||||
* @property {number} [filter=0] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
|
||||
* @property {number} [maxDistance=0.0] The max distance at which this Pick will intersect. 0.0 = no max. < 0.0 is invalid.
|
||||
*/
|
||||
unsigned int PickScriptingInterface::createStylusPick(const QVariant& properties) {
|
||||
|
@ -132,7 +138,7 @@ unsigned int PickScriptingInterface::createStylusPick(const QVariant& properties
|
|||
|
||||
PickFilter filter = PickFilter();
|
||||
if (propMap["filter"].isValid()) {
|
||||
filter = PickFilter(propMap["filter"].toUInt());
|
||||
filter = getPickFilter(propMap["filter"].toUInt());
|
||||
}
|
||||
|
||||
float maxDistance = 0.0f;
|
||||
|
@ -153,7 +159,7 @@ unsigned int PickScriptingInterface::createStylusPick(const QVariant& properties
|
|||
* A set of properties that can be passed to {@link Picks.createPick} to create a new Parabola Pick.
|
||||
* @typedef {object} Picks.ParabolaPickProperties
|
||||
* @property {boolean} [enabled=false] If this Pick should start enabled or not. Disabled Picks do not updated their pick results.
|
||||
* @property {number} [filter=Picks.PICK_NOTHING] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
|
||||
* @property {number} [filter=0] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
|
||||
* @property {number} [maxDistance=0.0] The max distance at which this Pick will intersect. 0.0 = no max. < 0.0 is invalid.
|
||||
* @property {Uuid} parentID - The ID of the parent, either an avatar, an entity, an overlay, or a pick.
|
||||
* @property {number} [parentJointIndex=0] - The joint of the parent to parent to, for example, the joints on the model of an avatar. (default = 0, no joint)
|
||||
|
@ -178,7 +184,7 @@ unsigned int PickScriptingInterface::createParabolaPick(const QVariant& properti
|
|||
|
||||
PickFilter filter = PickFilter();
|
||||
if (propMap["filter"].isValid()) {
|
||||
filter = PickFilter(propMap["filter"].toUInt());
|
||||
filter = getPickFilter(propMap["filter"].toUInt());
|
||||
}
|
||||
|
||||
float maxDistance = 0.0f;
|
||||
|
@ -250,7 +256,7 @@ unsigned int PickScriptingInterface::createParabolaPick(const QVariant& properti
|
|||
|
||||
* @typedef {object} Picks.CollisionPickProperties
|
||||
* @property {boolean} [enabled=false] If this Pick should start enabled or not. Disabled Picks do not updated their pick results.
|
||||
* @property {number} [filter=Picks.PICK_NOTHING] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
|
||||
* @property {number} [filter=0] The filter for this Pick to use, constructed using filter flags combined using bitwise OR.
|
||||
* @property {Shape} shape - The information about the collision region's size and shape. Dimensions are in world space, but will scale with the parent if defined.
|
||||
* @property {Vec3} position - The position of the collision region, relative to a parent if defined.
|
||||
* @property {Quat} orientation - The orientation of the collision region, relative to a parent if defined.
|
||||
|
@ -273,7 +279,7 @@ unsigned int PickScriptingInterface::createCollisionPick(const QVariant& propert
|
|||
|
||||
PickFilter filter = PickFilter();
|
||||
if (propMap["filter"].isValid()) {
|
||||
filter = PickFilter(propMap["filter"].toUInt());
|
||||
filter = getPickFilter(propMap["filter"].toUInt());
|
||||
}
|
||||
|
||||
float maxDistance = 0.0f;
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#include <DependencyManager.h>
|
||||
#include <PhysicsEngine.h>
|
||||
#include <Pick.h>
|
||||
#include <PickFilter.h>
|
||||
|
||||
/**jsdoc
|
||||
* The Picks API lets you create and manage objects for repeatedly calculating intersections in different ways.
|
||||
|
@ -23,41 +24,62 @@
|
|||
* @hifi-interface
|
||||
* @hifi-client-entity
|
||||
*
|
||||
* @property {number} PICK_NOTHING A filter flag. Don't intersect with anything. <em>Read-only.</em>
|
||||
* @property {number} PICK_ENTITIES A filter flag. Include entities when intersecting. <em>Read-only.</em>
|
||||
* @property {number} PICK_OVERLAYS A filter flag. Include overlays when intersecting. <em>Read-only.</em>
|
||||
* @property {number} PICK_AVATARS A filter flag. Include avatars when intersecting. <em>Read-only.</em>
|
||||
* @property {number} PICK_HUD A filter flag. Include the HUD sphere when intersecting in HMD mode. <em>Read-only.</em>
|
||||
* @property {number} PICK_COARSE A filter flag. Pick against coarse meshes, instead of exact meshes. <em>Read-only.</em>
|
||||
* @property {number} PICK_INCLUDE_INVISIBLE A filter flag. Include invisible objects when intersecting. <em>Read-only.</em>
|
||||
* @property {number} PICK_INCLUDE_NONCOLLIDABLE A filter flag. Include non-collidable objects when intersecting.
|
||||
* <em>Read-only.</em>
|
||||
* @property {number} PICK_ALL_INTERSECTIONS <em>Read-only.</em>
|
||||
* @property {number} INTERSECTED_NONE An intersection type. Intersected nothing with the given filter flags.
|
||||
* <em>Read-only.</em>
|
||||
* @property {number} PICK_ENTITIES A filter flag. Include domain and avatar entities when intersecting. <em>Read-only.</em>. Deprecated.
|
||||
* @property {number} PICK_OVERLAYS A filter flag. Include local entities when intersecting. <em>Read-only.</em>. Deprecated.
|
||||
*
|
||||
* @property {number} PICK_DOMAIN_ENTITIES A filter flag. Include domain entities when intersecting. <em>Read-only.</em>.
|
||||
* @property {number} PICK_AVATAR_ENTITIES A filter flag. Include avatar entities when intersecting. <em>Read-only.</em>.
|
||||
* @property {number} PICK_LOCAL_ENTITIES A filter flag. Include local entities when intersecting. <em>Read-only.</em>.
|
||||
* @property {number} PICK_AVATARS A filter flag. Include avatars when intersecting. <em>Read-only.</em>.
|
||||
* @property {number} PICK_HUD A filter flag. Include the HUD sphere when intersecting in HMD mode. <em>Read-only.</em>.
|
||||
*
|
||||
* @property {number} PICK_INCLUDE_VISIBLE A filter flag. Include visible objects when intersecting. <em>Read-only.</em>.
|
||||
* @property {number} PICK_INCLUDE_INVISIBLE A filter flag. Include invisible objects when intersecting. <em>Read-only.</em>.
|
||||
*
|
||||
* @property {number} PICK_INCLUDE_COLLIDABLE A filter flag. Include collidable objects when intersecting. <em>Read-only.</em>.
|
||||
* @property {number} PICK_INCLUDE_NONCOLLIDABLE A filter flag. Include non-collidable objects when intersecting. <em>Read-only.</em>.
|
||||
*
|
||||
* @property {number} PICK_PRECISE A filter flag. Pick against exact meshes. <em>Read-only.</em>.
|
||||
* @property {number} PICK_COARSE A filter flag. Pick against coarse meshes. <em>Read-only.</em>.
|
||||
*
|
||||
* @property {number} PICK_ALL_INTERSECTIONS <em>Read-only.</em>.
|
||||
*
|
||||
* @property {number} INTERSECTED_NONE An intersection type. Intersected nothing with the given filter flags. <em>Read-only.</em>
|
||||
* @property {number} INTERSECTED_ENTITY An intersection type. Intersected an entity. <em>Read-only.</em>
|
||||
* @property {number} INTERSECTED_OVERLAY An intersection type. Intersected an overlay. <em>Read-only.</em>
|
||||
* @property {number} INTERSECTED_AVATAR An intersection type. Intersected an avatar. <em>Read-only.</em>
|
||||
* @property {number} INTERSECTED_HUD An intersection type. Intersected the HUD sphere. <em>Read-only.</em>
|
||||
* @property {number} perFrameTimeBudget - The max number of usec to spend per frame updating Pick results. <em>Read-only.</em>
|
||||
* @property {number} perFrameTimeBudget - The max number of usec to spend per frame updating Pick results.
|
||||
*/
|
||||
|
||||
class PickScriptingInterface : public QObject, public Dependency {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(unsigned int PICK_NOTHING READ PICK_NOTHING CONSTANT)
|
||||
Q_PROPERTY(unsigned int PICK_ENTITIES READ PICK_ENTITIES CONSTANT)
|
||||
Q_PROPERTY(unsigned int PICK_OVERLAYS READ PICK_OVERLAYS CONSTANT)
|
||||
|
||||
Q_PROPERTY(unsigned int PICK_DOMAIN_ENTITIES READ PICK_DOMAIN_ENTITIES CONSTANT)
|
||||
Q_PROPERTY(unsigned int PICK_AVATAR_ENTITIES READ PICK_AVATAR_ENTITIES CONSTANT)
|
||||
Q_PROPERTY(unsigned int PICK_LOCAL_ENTITIES READ PICK_LOCAL_ENTITIES CONSTANT)
|
||||
Q_PROPERTY(unsigned int PICK_AVATARS READ PICK_AVATARS CONSTANT)
|
||||
Q_PROPERTY(unsigned int PICK_HUD READ PICK_HUD CONSTANT)
|
||||
Q_PROPERTY(unsigned int PICK_COARSE READ PICK_COARSE CONSTANT)
|
||||
|
||||
Q_PROPERTY(unsigned int PICK_INCLUDE_VISIBLE READ PICK_INCLUDE_VISIBLE CONSTANT)
|
||||
Q_PROPERTY(unsigned int PICK_INCLUDE_INVISIBLE READ PICK_INCLUDE_INVISIBLE CONSTANT)
|
||||
|
||||
Q_PROPERTY(unsigned int PICK_INCLUDE_COLLIDABLE READ PICK_INCLUDE_COLLIDABLE CONSTANT)
|
||||
Q_PROPERTY(unsigned int PICK_INCLUDE_NONCOLLIDABLE READ PICK_INCLUDE_NONCOLLIDABLE CONSTANT)
|
||||
|
||||
Q_PROPERTY(unsigned int PICK_PRECISE READ PICK_PRECISE CONSTANT)
|
||||
Q_PROPERTY(unsigned int PICK_COARSE READ PICK_COARSE CONSTANT)
|
||||
|
||||
Q_PROPERTY(unsigned int PICK_ALL_INTERSECTIONS READ PICK_ALL_INTERSECTIONS CONSTANT)
|
||||
|
||||
Q_PROPERTY(unsigned int INTERSECTED_NONE READ INTERSECTED_NONE CONSTANT)
|
||||
Q_PROPERTY(unsigned int INTERSECTED_ENTITY READ INTERSECTED_ENTITY CONSTANT)
|
||||
Q_PROPERTY(unsigned int INTERSECTED_OVERLAY READ INTERSECTED_OVERLAY CONSTANT)
|
||||
Q_PROPERTY(unsigned int INTERSECTED_AVATAR READ INTERSECTED_AVATAR CONSTANT)
|
||||
Q_PROPERTY(unsigned int INTERSECTED_HUD READ INTERSECTED_HUD CONSTANT)
|
||||
Q_PROPERTY(unsigned int perFrameTimeBudget READ getPerFrameTimeBudget WRITE setPerFrameTimeBudget)
|
||||
SINGLETON_DEPENDENCY
|
||||
|
||||
public:
|
||||
|
@ -72,11 +94,13 @@ public:
|
|||
* Adds a new Pick.
|
||||
* Different {@link PickType}s use different properties, and within one PickType, the properties you choose can lead to a wide range of behaviors. For example,
|
||||
* with PickType.Ray, depending on which optional parameters you pass, you could create a Static Ray Pick, a Mouse Ray Pick, or a Joint Ray Pick.
|
||||
* Picks created with this method always intersect at least visible and collidable things
|
||||
* @function Picks.createPick
|
||||
* @param {PickType} type A PickType that specifies the method of picking to use
|
||||
* @param {Picks.RayPickProperties|Picks.StylusPickProperties|Picks.ParabolaPickProperties|Picks.CollisionPickProperties} properties A PickProperties object, containing all the properties for initializing this Pick
|
||||
* @returns {number} The ID of the created Pick. Used for managing the Pick. 0 if invalid.
|
||||
*/
|
||||
// TODO: expand Pointers to be able to be fully configurable with PickFilters
|
||||
Q_INVOKABLE unsigned int createPick(const PickQuery::PickType type, const QVariant& properties);
|
||||
|
||||
/**jsdoc
|
||||
|
@ -227,61 +251,80 @@ public:
|
|||
*/
|
||||
Q_INVOKABLE bool isMouse(unsigned int uid);
|
||||
|
||||
// FIXME: Move to other property definitions.
|
||||
Q_PROPERTY(unsigned int perFrameTimeBudget READ getPerFrameTimeBudget WRITE setPerFrameTimeBudget)
|
||||
|
||||
unsigned int getPerFrameTimeBudget() const;
|
||||
void setPerFrameTimeBudget(unsigned int numUsecs);
|
||||
|
||||
public slots:
|
||||
|
||||
/**jsdoc
|
||||
* @function Picks.PICK_NOTHING
|
||||
* @returns {number}
|
||||
*/
|
||||
static constexpr unsigned int PICK_NOTHING() { return 0; }
|
||||
|
||||
/**jsdoc
|
||||
* @function Picks.PICK_ENTITIES
|
||||
* @returns {number}
|
||||
*/
|
||||
static constexpr unsigned int PICK_ENTITIES() { return PickFilter::getBitMask(PickFilter::FlagBit::PICK_ENTITIES); }
|
||||
|
||||
static constexpr unsigned int PICK_ENTITIES() { return PickFilter::getBitMask(PickFilter::FlagBit::DOMAIN_ENTITIES) | PickFilter::getBitMask(PickFilter::FlagBit::AVATAR_ENTITIES); }
|
||||
/**jsdoc
|
||||
* @function Picks.PICK_OVERLAYS
|
||||
* @returns {number}
|
||||
*/
|
||||
static constexpr unsigned int PICK_OVERLAYS() { return PickFilter::getBitMask(PickFilter::FlagBit::PICK_OVERLAYS); }
|
||||
static constexpr unsigned int PICK_OVERLAYS() { return PickFilter::getBitMask(PickFilter::FlagBit::LOCAL_ENTITIES); }
|
||||
|
||||
/**jsdoc
|
||||
* @function Picks.PICK_DOMAIN_ENTITIES
|
||||
* @returns {number}
|
||||
*/
|
||||
static constexpr unsigned int PICK_DOMAIN_ENTITIES() { return PickFilter::getBitMask(PickFilter::FlagBit::DOMAIN_ENTITIES); }
|
||||
/**jsdoc
|
||||
* @function Picks.PICK_AVATAR_ENTITIES
|
||||
* @returns {number}
|
||||
*/
|
||||
static constexpr unsigned int PICK_AVATAR_ENTITIES() { return PickFilter::getBitMask(PickFilter::FlagBit::AVATAR_ENTITIES); }
|
||||
/**jsdoc
|
||||
* @function Picks.PICK_LOCAL_ENTITIES
|
||||
* @returns {number}
|
||||
*/
|
||||
static constexpr unsigned int PICK_LOCAL_ENTITIES() { return PickFilter::getBitMask(PickFilter::FlagBit::LOCAL_ENTITIES); }
|
||||
/**jsdoc
|
||||
* @function Picks.PICK_AVATARS
|
||||
* @returns {number}
|
||||
*/
|
||||
static constexpr unsigned int PICK_AVATARS() { return PickFilter::getBitMask(PickFilter::FlagBit::PICK_AVATARS); }
|
||||
|
||||
static constexpr unsigned int PICK_AVATARS() { return PickFilter::getBitMask(PickFilter::FlagBit::AVATARS); }
|
||||
/**jsdoc
|
||||
* @function Picks.PICK_HUD
|
||||
* @returns {number}
|
||||
*/
|
||||
static constexpr unsigned int PICK_HUD() { return PickFilter::getBitMask(PickFilter::FlagBit::PICK_HUD); }
|
||||
static constexpr unsigned int PICK_HUD() { return PickFilter::getBitMask(PickFilter::FlagBit::HUD); }
|
||||
|
||||
/**jsdoc
|
||||
* @function Picks.PICK_COARSE
|
||||
* @function Picks.PICK_INCLUDE_VISIBLE
|
||||
* @returns {number}
|
||||
*/
|
||||
static constexpr unsigned int PICK_COARSE() { return PickFilter::getBitMask(PickFilter::FlagBit::PICK_COARSE); }
|
||||
|
||||
static constexpr unsigned int PICK_INCLUDE_VISIBLE() { return PickFilter::getBitMask(PickFilter::FlagBit::VISIBLE); }
|
||||
/**jsdoc
|
||||
* @function Picks.PICK_INCLUDE_INVISIBLE
|
||||
* @returns {number}
|
||||
*/
|
||||
static constexpr unsigned int PICK_INCLUDE_INVISIBLE() { return PickFilter::getBitMask(PickFilter::FlagBit::PICK_INCLUDE_INVISIBLE); }
|
||||
static constexpr unsigned int PICK_INCLUDE_INVISIBLE() { return PickFilter::getBitMask(PickFilter::FlagBit::INVISIBLE); }
|
||||
|
||||
/**jsdoc
|
||||
* @function Picks.PICK_INCLUDE_COLLIDABLE
|
||||
* @returns {number}
|
||||
*/
|
||||
static constexpr unsigned int PICK_INCLUDE_COLLIDABLE() { return PickFilter::getBitMask(PickFilter::FlagBit::COLLIDABLE); }
|
||||
/**jsdoc
|
||||
* @function Picks.PICK_INCLUDE_NONCOLLIDABLE
|
||||
* @returns {number}
|
||||
*/
|
||||
static constexpr unsigned int PICK_INCLUDE_NONCOLLIDABLE() { return PickFilter::getBitMask(PickFilter::FlagBit::PICK_INCLUDE_NONCOLLIDABLE); }
|
||||
static constexpr unsigned int PICK_INCLUDE_NONCOLLIDABLE() { return PickFilter::getBitMask(PickFilter::FlagBit::NONCOLLIDABLE); }
|
||||
|
||||
/**jsdoc
|
||||
* @function Picks.PICK_PRECISE
|
||||
* @returns {number}
|
||||
*/
|
||||
static constexpr unsigned int PICK_PRECISE() { return PickFilter::getBitMask(PickFilter::FlagBit::PRECISE); }
|
||||
/**jsdoc
|
||||
* @function Picks.PICK_COARSE
|
||||
* @returns {number}
|
||||
*/
|
||||
static constexpr unsigned int PICK_COARSE() { return PickFilter::getBitMask(PickFilter::FlagBit::COARSE); }
|
||||
|
||||
/**jsdoc
|
||||
* @function Picks.PICK_ALL_INTERSECTIONS
|
||||
|
|
|
@ -41,10 +41,12 @@ public:
|
|||
* @property {string} button Which button to trigger. "Primary", "Secondary", "Tertiary", and "Focus" are currently supported. Only "Primary" will trigger clicks on web surfaces. If "Focus" is triggered,
|
||||
* it will try to set the entity or overlay focus to the object at which the Pointer is aimed. Buttons besides the first three will still trigger events, but event.button will be "None".
|
||||
*/
|
||||
|
||||
/**jsdoc
|
||||
* Adds a new Pointer
|
||||
* Different {@link PickType}s use different properties, and within one PickType, the properties you choose can lead to a wide range of behaviors. For example,
|
||||
* with PickType.Ray, depending on which optional parameters you pass, you could create a Static Ray Pointer, a Mouse Ray Pointer, or a Joint Ray Pointer.
|
||||
* Pointers created with this method always intersect at least visible and collidable things
|
||||
* @function Pointers.createPointer
|
||||
* @param {PickType} type A PickType that specifies the method of picking to use
|
||||
* @param {Pointers.LaserPointerProperties|Pointers.StylusPointerProperties|Pointers.ParabolaPointerProperties} properties A PointerProperties object, containing all the properties for initializing this Pointer <b>and</b> the {@link Picks.PickProperties} for the Pick that
|
||||
|
@ -58,21 +60,21 @@ public:
|
|||
* dimensions: {x:0.5, y:0.5, z:0.5},
|
||||
* solid: true,
|
||||
* color: {red:0, green:255, blue:0},
|
||||
* ignoreRayIntersection: true
|
||||
* ignorePickIntersection: true
|
||||
* };
|
||||
* var end2 = {
|
||||
* type: "sphere",
|
||||
* dimensions: {x:0.5, y:0.5, z:0.5},
|
||||
* solid: true,
|
||||
* color: {red:255, green:0, blue:0},
|
||||
* ignoreRayIntersection: true
|
||||
* ignorePickIntersection: true
|
||||
* };
|
||||
*
|
||||
* var renderStates = [ {name: "test", end: end} ];
|
||||
* var defaultRenderStates = [ {name: "test", distance: 10.0, end: end2} ];
|
||||
* var pointer = Pointers.createPointer(PickType.Ray, {
|
||||
* joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND",
|
||||
* filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE,
|
||||
* filter: Picks.PICK_LOCAL_ENTITIES | Picks.PICK_DOMAIN_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE,
|
||||
* renderStates: renderStates,
|
||||
* defaultRenderStates: defaultRenderStates,
|
||||
* distanceScaleEnd: true,
|
||||
|
@ -82,6 +84,7 @@ public:
|
|||
* });
|
||||
* Pointers.setRenderState(pointer, "test");
|
||||
*/
|
||||
// TODO: expand Pointers to be able to be fully configurable with PickFilters
|
||||
Q_INVOKABLE unsigned int createPointer(const PickQuery::PickType& type, const QVariant& properties);
|
||||
|
||||
/**jsdoc
|
||||
|
|
|
@ -27,10 +27,15 @@ PickRay RayPick::getMathematicalPick() const {
|
|||
}
|
||||
|
||||
PickResultPointer RayPick::getEntityIntersection(const PickRay& pick) {
|
||||
bool precisionPicking = !(getFilter().doesPickCoarse() || DependencyManager::get<PickManager>()->getForceCoarsePicking());
|
||||
PickFilter searchFilter = getFilter();
|
||||
if (DependencyManager::get<PickManager>()->getForceCoarsePicking()) {
|
||||
searchFilter.setFlag(PickFilter::COARSE, true);
|
||||
searchFilter.setFlag(PickFilter::PRECISE, false);
|
||||
}
|
||||
|
||||
RayToEntityIntersectionResult entityRes =
|
||||
DependencyManager::get<EntityScriptingInterface>()->findRayIntersectionVector(pick, precisionPicking,
|
||||
getIncludeItemsAs<EntityItemID>(), getIgnoreItemsAs<EntityItemID>(), !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable());
|
||||
DependencyManager::get<EntityScriptingInterface>()->evalRayIntersectionVector(pick, searchFilter,
|
||||
getIncludeItemsAs<EntityItemID>(), getIgnoreItemsAs<EntityItemID>());
|
||||
if (entityRes.intersects) {
|
||||
return std::make_shared<RayPickResult>(IntersectionType::ENTITY, entityRes.entityID, entityRes.distance, entityRes.intersection, pick, entityRes.surfaceNormal, entityRes.extraInfo);
|
||||
} else {
|
||||
|
@ -39,7 +44,7 @@ PickResultPointer RayPick::getEntityIntersection(const PickRay& pick) {
|
|||
}
|
||||
|
||||
PickResultPointer RayPick::getOverlayIntersection(const PickRay& pick) {
|
||||
bool precisionPicking = !(getFilter().doesPickCoarse() || DependencyManager::get<PickManager>()->getForceCoarsePicking());
|
||||
bool precisionPicking = !(getFilter().isCoarse() || DependencyManager::get<PickManager>()->getForceCoarsePicking());
|
||||
RayToOverlayIntersectionResult overlayRes =
|
||||
qApp->getOverlays().findRayIntersectionVector(pick, precisionPicking,
|
||||
getIncludeItemsAs<OverlayID>(), getIgnoreItemsAs<OverlayID>(), !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable());
|
||||
|
|
|
@ -19,14 +19,13 @@
|
|||
#include "PickScriptingInterface.h"
|
||||
|
||||
/**jsdoc
|
||||
* Synonym for {@link Picks} as used for ray picks.
|
||||
* Synonym for {@link Picks} as used for ray picks. Deprecated.
|
||||
*
|
||||
* @namespace RayPick
|
||||
*
|
||||
* @hifi-interface
|
||||
* @hifi-client-entity
|
||||
*
|
||||
* @property {number} PICK_NOTHING <em>Read-only.</em>
|
||||
* @property {number} PICK_ENTITIES <em>Read-only.</em>
|
||||
* @property {number} PICK_OVERLAYS <em>Read-only.</em>
|
||||
* @property {number} PICK_AVATARS <em>Read-only.</em>
|
||||
|
@ -44,7 +43,6 @@
|
|||
|
||||
class RayPickScriptingInterface : public QObject, public Dependency {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(unsigned int PICK_NOTHING READ PICK_NOTHING CONSTANT)
|
||||
Q_PROPERTY(unsigned int PICK_ENTITIES READ PICK_ENTITIES CONSTANT)
|
||||
Q_PROPERTY(unsigned int PICK_OVERLAYS READ PICK_OVERLAYS CONSTANT)
|
||||
Q_PROPERTY(unsigned int PICK_AVATARS READ PICK_AVATARS CONSTANT)
|
||||
|
@ -140,12 +138,6 @@ public:
|
|||
|
||||
public slots:
|
||||
|
||||
/**jsdoc
|
||||
* @function RayPick.PICK_NOTHING
|
||||
* @returns {number}
|
||||
*/
|
||||
static unsigned int PICK_NOTHING() { return PickScriptingInterface::PICK_NOTHING(); }
|
||||
|
||||
/**jsdoc
|
||||
* @function RayPick.PICK_ENTITIES
|
||||
* @returns {number}
|
||||
|
|
|
@ -61,7 +61,7 @@ OverlayID StylusPointer::buildStylusOverlay(const QVariantMap& properties) {
|
|||
overlayProperties["loadPriority"] = 10.0f;
|
||||
overlayProperties["solid"] = true;
|
||||
overlayProperties["visible"] = false;
|
||||
overlayProperties["ignoreRayIntersection"] = true;
|
||||
overlayProperties["ignorePickIntersection"] = true;
|
||||
overlayProperties["drawInFront"] = false;
|
||||
|
||||
return qApp->getOverlays().addOverlay("model", overlayProperties);
|
||||
|
|
|
@ -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
|
@ -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();
|
||||
}
|