diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java index 2a008d7950..2475c4d887 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java @@ -98,13 +98,17 @@ public class FriendsFragment extends Fragment { mUsersAdapter.setListener(new UserListAdapter.AdapterListener() { @Override - public void onEmptyAdapter() { - mSwipeRefreshLayout.setRefreshing(false); + public void onEmptyAdapter(boolean shouldStopRefreshing) { + if (shouldStopRefreshing) { + mSwipeRefreshLayout.setRefreshing(false); + } } @Override - public void onNonEmptyAdapter() { - mSwipeRefreshLayout.setRefreshing(false); + public void onNonEmptyAdapter(boolean shouldStopRefreshing) { + if (shouldStopRefreshing) { + mSwipeRefreshLayout.setRefreshing(false); + } } @Override @@ -115,6 +119,8 @@ public class FriendsFragment extends Fragment { mUsersView.setAdapter(mUsersAdapter); + mUsersAdapter.startLoad(); + mSlidingUpPanelLayout.setFadeOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java index 7bd373cf1d..86b8625cfe 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java @@ -76,18 +76,22 @@ public class HomeFragment extends Fragment { }); mDomainAdapter.setListener(new DomainAdapter.AdapterListener() { @Override - public void onEmptyAdapter() { + public void onEmptyAdapter(boolean shouldStopRefreshing) { searchNoResultsView.setText(R.string.search_no_results); searchNoResultsView.setVisibility(View.VISIBLE); mDomainsView.setVisibility(View.GONE); - mSwipeRefreshLayout.setRefreshing(false); + if (shouldStopRefreshing) { + mSwipeRefreshLayout.setRefreshing(false); + } } @Override - public void onNonEmptyAdapter() { + public void onNonEmptyAdapter(boolean shouldStopRefreshing) { searchNoResultsView.setVisibility(View.GONE); mDomainsView.setVisibility(View.VISIBLE); - mSwipeRefreshLayout.setRefreshing(false); + if (shouldStopRefreshing) { + mSwipeRefreshLayout.setRefreshing(false); + } } @Override @@ -96,11 +100,20 @@ public class HomeFragment extends Fragment { } }); mDomainsView.setAdapter(mDomainAdapter); + mDomainAdapter.startLoad(); mSearchView = rootView.findViewById(R.id.searchView); mSearchIconView = rootView.findViewById(R.id.search_mag_icon); mClearSearch = rootView.findViewById(R.id.search_clear); + getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + + return rootView; + } + + @Override + public void onStart() { + super.onStart(); mSearchView.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} @@ -142,10 +155,6 @@ public class HomeFragment extends Fragment { mDomainAdapter.loadDomains(mSearchView.getText().toString(), true); } }); - - getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); - - return rootView; } @Override diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java b/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java index 71d634e9ea..78251ac4a4 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java @@ -12,6 +12,7 @@ import android.widget.TextView; import com.squareup.picasso.Picasso; +import java.util.Arrays; import java.util.List; import io.highfidelity.hifiinterface.R; @@ -36,19 +37,41 @@ public class DomainAdapter extends RecyclerView.Adapter 0) { + mDomains = Arrays.copyOf(DOMAINS_TMP_CACHE, DOMAINS_TMP_CACHE.length); + notifyDataSetChanged(); + if (mAdapterListener != null) { + if (mDomains.length == 0) { + mAdapterListener.onEmptyAdapter(false); + } else { + mAdapterListener.onNonEmptyAdapter(false); + } + } + } + } + } + public void loadDomains(String filterText, boolean forceRefresh) { domainProvider.retrieve(filterText, new DomainProvider.DomainCallback() { @Override @@ -60,13 +83,18 @@ public class DomainAdapter extends RecyclerView.Adapter USERS_TMP_CACHE; + public UserListAdapter(Context c, UsersProvider usersProvider) { mContext = c; mInflater = LayoutInflater.from(mContext); mProvider = usersProvider; - loadUsers(); } public void setListener(AdapterListener adapterListener) { mAdapterListener = adapterListener; } + public void startLoad() { + useTmpCachedUsers(); + loadUsers(); + } + + private void useTmpCachedUsers() { + synchronized (this) { + if (USERS_TMP_CACHE != null && USERS_TMP_CACHE.size() > 0) { + mUsers = new ArrayList<>(USERS_TMP_CACHE.size()); + mUsers.addAll(USERS_TMP_CACHE); + notifyDataSetChanged(); + if (mAdapterListener != null) { + if (mUsers.isEmpty()) { + mAdapterListener.onEmptyAdapter(false); + } else { + mAdapterListener.onNonEmptyAdapter(false); + } + } + } + } + } + public void loadUsers() { mProvider.retrieve(new UsersProvider.UsersCallback() { @Override public void retrieveOk(List users) { mUsers = new ArrayList<>(users); notifyDataSetChanged(); - if (mAdapterListener != null) { - if (mUsers.isEmpty()) { - mAdapterListener.onEmptyAdapter(); - } else { - mAdapterListener.onNonEmptyAdapter(); + + synchronized (this) { + USERS_TMP_CACHE = new ArrayList<>(mUsers.size()); + USERS_TMP_CACHE.addAll(mUsers); + + if (mAdapterListener != null) { + if (mUsers.isEmpty()) { + mAdapterListener.onEmptyAdapter(true); + } else { + mAdapterListener.onNonEmptyAdapter(true); + } } } } @@ -240,8 +269,9 @@ public class UserListAdapter extends RecyclerView.AdapterrenderableForEntityId(entityID); if (renderable) { @@ -1881,6 +1885,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo return false; }); EntityTree::setRemoveMaterialFromEntityOperator([this](const QUuid& entityID, graphics::MaterialPointer material, const std::string& parentMaterialName) { + if (_aboutToQuit) { + return false; + } + // try to find the renderable auto renderable = getEntities()->renderableForEntityId(entityID); if (renderable) { diff --git a/interface/src/Application.h b/interface/src/Application.h index ae7e686f35..aa8323cd38 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -312,6 +312,9 @@ public: Q_INVOKABLE void copyToClipboard(const QString& text); + int getOtherAvatarsReplicaCount() { return DependencyManager::get()->getReplicaCount(); } + void setOtherAvatarsReplicaCount(int count) { DependencyManager::get()->setReplicaCount(count); } + #if defined(Q_OS_ANDROID) void beforeEnterBackground(); void enterBackground(); diff --git a/interface/src/raypick/PathPointer.cpp b/interface/src/raypick/PathPointer.cpp index 685611d77b..d434c667de 100644 --- a/interface/src/raypick/PathPointer.cpp +++ b/interface/src/raypick/PathPointer.cpp @@ -105,7 +105,7 @@ PickResultPointer PathPointer::getVisualPickResult(const PickResultPointer& pick glm::mat4 entityMat = createMatFromQuatAndPos(props.getRotation(), props.getPosition()); glm::mat4 finalPosAndRotMat = entityMat * _lockEndObject.offsetMat; pos = extractTranslation(finalPosAndRotMat); - rot = glmExtractRotation(finalPosAndRotMat); + rot = props.getRotation(); dim = props.getDimensions(); registrationPoint = props.getRegistrationPoint(); } @@ -350,4 +350,4 @@ glm::vec2 PathPointer::findPos2D(const PickedObject& pickedObject, const glm::ve default: return glm::vec2(NAN); } -} \ No newline at end of file +} diff --git a/interface/src/scripting/TestScriptingInterface.cpp b/interface/src/scripting/TestScriptingInterface.cpp index 430441226f..52f6a3ebc0 100644 --- a/interface/src/scripting/TestScriptingInterface.cpp +++ b/interface/src/scripting/TestScriptingInterface.cpp @@ -190,4 +190,12 @@ void TestScriptingInterface::saveObject(QVariant variant, const QString& filenam void TestScriptingInterface::showMaximized() { qApp->getWindow()->showMaximized(); +} + +void TestScriptingInterface::setOtherAvatarsReplicaCount(int count) { + qApp->setOtherAvatarsReplicaCount(count); +} + +int TestScriptingInterface::getOtherAvatarsReplicaCount() { + return qApp->getOtherAvatarsReplicaCount(); } \ No newline at end of file diff --git a/interface/src/scripting/TestScriptingInterface.h b/interface/src/scripting/TestScriptingInterface.h index c47e39d1f3..4a1d1a3eeb 100644 --- a/interface/src/scripting/TestScriptingInterface.h +++ b/interface/src/scripting/TestScriptingInterface.h @@ -149,6 +149,20 @@ public slots: */ void showMaximized(); + /**jsdoc + * Values higher than 0 will create replicas of other-avatars when entering a domain for testing purpouses + * @function Test.setOtherAvatarsReplicaCount + * @param {number} count - Number of replicas we want to create + */ + Q_INVOKABLE void setOtherAvatarsReplicaCount(int count); + + /**jsdoc + * Return the number of replicas that are being created of other-avatars when entering a domain + * @function Test.getOtherAvatarsReplicaCount + * @returns {number} Current number of replicas of other-avatars. + */ + Q_INVOKABLE int getOtherAvatarsReplicaCount(); + private: bool waitForCondition(qint64 maxWaitMs, std::function condition); QString _testResultsLocation; diff --git a/libraries/animation/src/AnimBlendLinearMove.cpp b/libraries/animation/src/AnimBlendLinearMove.cpp index 6313d4cbe9..42098eb072 100644 --- a/libraries/animation/src/AnimBlendLinearMove.cpp +++ b/libraries/animation/src/AnimBlendLinearMove.cpp @@ -156,6 +156,10 @@ void AnimBlendLinearMove::setFrameAndPhase(float dt, float alpha, int prevPoseIn // integrate phase forward in time. _phase += omega * dt; + if (_phase < 0.0f) { + _phase = 0.0f; + } + // detect loop trigger events if (_phase >= 1.0f) { triggersOut.setTrigger(_id + "Loop"); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 7d3ec39c4f..37c3d08c6c 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -458,7 +458,6 @@ protected: glm::vec3 _lastAngularVelocity; glm::vec3 _angularAcceleration; glm::quat _lastOrientation; - glm::vec3 _worldUpDirection { Vectors::UP }; bool _moving { false }; ///< set when position is changing diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 262bf2a567..b86398501e 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -918,7 +918,18 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { PACKET_READ_CHECK(AvatarGlobalPosition, sizeof(AvatarDataPacket::AvatarGlobalPosition)); auto data = reinterpret_cast(sourceBuffer); - auto newValue = glm::vec3(data->globalPosition[0], data->globalPosition[1], data->globalPosition[2]); + + glm::vec3 offset = glm::vec3(0.0f, 0.0f, 0.0f); + + if (_replicaIndex > 0) { + const float SPACE_BETWEEN_AVATARS = 2.0f; + const int AVATARS_PER_ROW = 3; + int row = _replicaIndex % AVATARS_PER_ROW; + int col = floor(_replicaIndex / AVATARS_PER_ROW); + offset = glm::vec3(row * SPACE_BETWEEN_AVATARS, 0.0f, col * SPACE_BETWEEN_AVATARS); + } + + auto newValue = glm::vec3(data->globalPosition[0], data->globalPosition[1], data->globalPosition[2]) + offset; if (_globalPosition != newValue) { _globalPosition = newValue; _globalPositionChanged = usecTimestampNow(); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 42c2cf22f3..bb3b1fe2d3 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -337,6 +337,7 @@ enum KillAvatarReason : uint8_t { TheirAvatarEnteredYourBubble, YourAvatarEnteredTheirBubble }; + Q_DECLARE_METATYPE(KillAvatarReason); class QDataStream; @@ -1186,6 +1187,8 @@ public: virtual void addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) {} virtual void removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName) {} + void setReplicaIndex(int replicaIndex) { _replicaIndex = replicaIndex; } + int getReplicaIndex() { return _replicaIndex; } signals: @@ -1445,6 +1448,7 @@ protected: udt::SequenceNumber _identitySequenceNumber { 0 }; bool _hasProcessedFirstIdentity { false }; float _density; + int _replicaIndex { 0 }; // null unless MyAvatar or ScriptableAvatar sending traits data to mixer std::unique_ptr _clientTraitsHandler; diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 64b26131be..c437b56f32 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -21,6 +21,84 @@ #include "AvatarLogging.h" #include "AvatarTraits.h" + +void AvatarReplicas::addReplica(const QUuid& parentID, AvatarSharedPointer replica) { + if (parentID == QUuid()) { + return; + } + if (_replicasMap.find(parentID) == _replicasMap.end()) { + std::vector emptyReplicas = std::vector(); + _replicasMap.insert(std::pair>(parentID, emptyReplicas)); + } + auto &replicas = _replicasMap[parentID]; + replica->setReplicaIndex((int)replicas.size() + 1); + replicas.push_back(replica); +} + +std::vector AvatarReplicas::getReplicaIDs(const QUuid& parentID) { + std::vector ids; + if (_replicasMap.find(parentID) != _replicasMap.end()) { + auto &replicas = _replicasMap[parentID]; + for (int i = 0; i < (int)replicas.size(); i++) { + ids.push_back(replicas[i]->getID()); + } + } else if (_replicaCount > 0) { + for (int i = 0; i < _replicaCount; i++) { + ids.push_back(QUuid::createUuid()); + } + } + return ids; +} + +void AvatarReplicas::parseDataFromBuffer(const QUuid& parentID, const QByteArray& buffer) { + if (_replicasMap.find(parentID) != _replicasMap.end()) { + auto &replicas = _replicasMap[parentID]; + for (auto avatar : replicas) { + avatar->parseDataFromBuffer(buffer); + } + } +} + +void AvatarReplicas::removeReplicas(const QUuid& parentID) { + if (_replicasMap.find(parentID) != _replicasMap.end()) { + _replicasMap.erase(parentID); + } +} + +void AvatarReplicas::processAvatarIdentity(const QUuid& parentID, const QByteArray& identityData, bool& identityChanged, bool& displayNameChanged) { + if (_replicasMap.find(parentID) != _replicasMap.end()) { + auto &replicas = _replicasMap[parentID]; + for (auto avatar : replicas) { + avatar->processAvatarIdentity(identityData, identityChanged, displayNameChanged); + } + } +} +void AvatarReplicas::processTrait(const QUuid& parentID, AvatarTraits::TraitType traitType, QByteArray traitBinaryData) { + if (_replicasMap.find(parentID) != _replicasMap.end()) { + auto &replicas = _replicasMap[parentID]; + for (auto avatar : replicas) { + avatar->processTrait(traitType, traitBinaryData); + } + } +} +void AvatarReplicas::processDeletedTraitInstance(const QUuid& parentID, AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID) { + if (_replicasMap.find(parentID) != _replicasMap.end()) { + auto &replicas = _replicasMap[parentID]; + for (auto avatar : replicas) { + avatar->processDeletedTraitInstance(traitType, instanceID); + } + } +} +void AvatarReplicas::processTraitInstance(const QUuid& parentID, AvatarTraits::TraitType traitType, + AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData) { + if (_replicasMap.find(parentID) != _replicasMap.end()) { + auto &replicas = _replicasMap[parentID]; + for (auto avatar : replicas) { + avatar->processTraitInstance(traitType, instanceID, traitBinaryData); + } + } +} + AvatarHashMap::AvatarHashMap() { auto nodeList = DependencyManager::get(); @@ -64,6 +142,21 @@ bool AvatarHashMap::isAvatarInRange(const glm::vec3& position, const float range return false; } +void AvatarHashMap::setReplicaCount(int count) { + _replicas.setReplicaCount(count); + auto avatars = getAvatarIdentifiers(); + for (int i = 0; i < avatars.size(); i++) { + KillAvatarReason reason = KillAvatarReason::NoReason; + if (avatars[i] != QUuid()) { + removeAvatar(avatars[i], reason); + auto replicaIDs = _replicas.getReplicaIDs(avatars[i]); + for (auto id : replicaIDs) { + removeAvatar(id, reason); + } + } + } +} + int AvatarHashMap::numberOfAvatarsInRange(const glm::vec3& position, float rangeMeters) { auto hashCopy = getHashCopy(); auto rangeMeters2 = rangeMeters * rangeMeters; @@ -135,18 +228,25 @@ AvatarSharedPointer AvatarHashMap::parseAvatarData(QSharedPointer(); - bool isNewAvatar; if (sessionUUID != _lastOwnerSessionUUID && (!nodeList->isIgnoringNode(sessionUUID) || nodeList->getRequestsDomainListData())) { auto avatar = newOrExistingAvatar(sessionUUID, sendingNode, isNewAvatar); + if (isNewAvatar) { QWriteLocker locker(&_hashLock); _pendingAvatars.insert(sessionUUID, { std::chrono::steady_clock::now(), 0, avatar }); - } + auto replicaIDs = _replicas.getReplicaIDs(sessionUUID); + for (auto replicaID : replicaIDs) { + auto replicaAvatar = addAvatar(replicaID, sendingNode); + _replicas.addReplica(sessionUUID, replicaAvatar); + } + } // have the matching (or new) avatar parse the data from the packet int bytesRead = avatar->parseDataFromBuffer(byteArray); message->seek(positionBeforeRead + bytesRead); + _replicas.parseDataFromBuffer(sessionUUID, byteArray); + return avatar; } else { // create a dummy AvatarData class to throw this data on the ground @@ -191,10 +291,13 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer bool displayNameChanged = false; // In this case, the "sendingNode" is the Avatar Mixer. avatar->processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged); + _replicas.processAvatarIdentity(identityUUID, message->getMessage(), identityChanged, displayNameChanged); + } } void AvatarHashMap::processBulkAvatarTraits(QSharedPointer message, SharedNodePointer sendingNode) { + while (message->getBytesLeftToRead()) { // read the avatar ID to figure out which avatar this is for auto avatarID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); @@ -202,7 +305,6 @@ void AvatarHashMap::processBulkAvatarTraits(QSharedPointer mess // grab the avatar so we can ask it to process trait data bool isNewAvatar; auto avatar = newOrExistingAvatar(avatarID, sendingNode, isNewAvatar); - // read the first trait type for this avatar AvatarTraits::TraitType traitType; message->readPrimitive(&traitType); @@ -217,13 +319,14 @@ void AvatarHashMap::processBulkAvatarTraits(QSharedPointer mess AvatarTraits::TraitWireSize traitBinarySize; bool skipBinaryTrait = false; - if (AvatarTraits::isSimpleTrait(traitType)) { message->readPrimitive(&traitBinarySize); // check if this trait version is newer than what we already have for this avatar if (packetTraitVersion > lastProcessedVersions[traitType]) { - avatar->processTrait(traitType, message->read(traitBinarySize)); + auto traitData = message->read(traitBinarySize); + avatar->processTrait(traitType, traitData); + _replicas.processTrait(avatarID, traitType, traitData); lastProcessedVersions[traitType] = packetTraitVersion; } else { skipBinaryTrait = true; @@ -238,8 +341,11 @@ void AvatarHashMap::processBulkAvatarTraits(QSharedPointer mess if (packetTraitVersion > processedInstanceVersion) { if (traitBinarySize == AvatarTraits::DELETED_TRAIT_SIZE) { avatar->processDeletedTraitInstance(traitType, traitInstanceID); + _replicas.processDeletedTraitInstance(avatarID, traitType, traitInstanceID); } else { - avatar->processTraitInstance(traitType, traitInstanceID, message->read(traitBinarySize)); + auto traitData = message->read(traitBinarySize); + avatar->processTraitInstance(traitType, traitInstanceID, traitData); + _replicas.processTraitInstance(avatarID, traitType, traitInstanceID, traitData); } processedInstanceVersion = packetTraitVersion; } else { @@ -265,17 +371,31 @@ void AvatarHashMap::processKillAvatar(QSharedPointer message, S KillAvatarReason reason; message->readPrimitive(&reason); removeAvatar(sessionUUID, reason); + auto replicaIDs = _replicas.getReplicaIDs(sessionUUID); + for (auto id : replicaIDs) { + removeAvatar(id, reason); + } } void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason) { QWriteLocker locker(&_hashLock); + auto replicaIDs = _replicas.getReplicaIDs(sessionUUID); + _replicas.removeReplicas(sessionUUID); + for (auto id : replicaIDs) { + auto removedReplica = _avatarHash.take(id); + if (removedReplica) { + handleRemovedAvatar(removedReplica, removalReason); + } + } + _pendingAvatars.remove(sessionUUID); auto removedAvatar = _avatarHash.take(sessionUUID); if (removedAvatar) { handleRemovedAvatar(removedAvatar, removalReason); } + } void AvatarHashMap::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) { diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index ba16fa9568..0f847b2a61 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -41,6 +41,27 @@ * @hifi-assignment-client */ +class AvatarReplicas { +public: + AvatarReplicas() {} + void addReplica(const QUuid& parentID, AvatarSharedPointer replica); + std::vector getReplicaIDs(const QUuid& parentID); + void parseDataFromBuffer(const QUuid& parentID, const QByteArray& buffer); + void processAvatarIdentity(const QUuid& parentID, const QByteArray& identityData, bool& identityChanged, bool& displayNameChanged); + void removeReplicas(const QUuid& parentID); + void processTrait(const QUuid& parentID, AvatarTraits::TraitType traitType, QByteArray traitBinaryData); + void processDeletedTraitInstance(const QUuid& parentID, AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID); + void processTraitInstance(const QUuid& parentID, AvatarTraits::TraitType traitType, + AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData); + void setReplicaCount(int count) { _replicaCount = count; } + int getReplicaCount() { return _replicaCount; } + +private: + std::map> _replicasMap; + int _replicaCount { 0 }; +}; + + class AvatarHashMap : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY @@ -77,6 +98,9 @@ public: virtual AvatarSharedPointer getAvatarBySessionID(const QUuid& sessionID) const { return findAvatar(sessionID); } int numberOfAvatarsInRange(const glm::vec3& position, float rangeMeters); + void setReplicaCount(int count); + int getReplicaCount() { return _replicas.getReplicaCount(); }; + signals: /**jsdoc @@ -167,6 +191,8 @@ protected: mutable QReadWriteLock _hashLock; std::unordered_map _processedTraitVersions; + AvatarReplicas _replicas; + private: QUuid _lastOwnerSessionUUID; }; diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index e4563fda14..5e798ed680 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -14,7 +14,8 @@ PICK_MAX_DISTANCE, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD, DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, ensureDynamic, getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, Reticle, Overlays, isPointingAtUI - Picks, makeLaserLockInfo Xform, makeLaserParams, AddressManager, getEntityParents, Selection, DISPATCHER_HOVERING_LIST + Picks, makeLaserLockInfo Xform, makeLaserParams, AddressManager, getEntityParents, Selection, DISPATCHER_HOVERING_LIST, + worldPositionToRegistrationFrameMatrix */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -593,18 +594,9 @@ Script.include("/~/system/libraries/Xform.js"); this.calculateOffset = function(controllerData) { if (this.distanceHolding || this.distanceRotating) { - var targetProps = Entities.getEntityProperties(this.targetObject.entityID, [ - "position", - "rotation" - ]); - var zeroVector = { x: 0, y: 0, z:0, w: 0 }; - var intersection = controllerData.rayPicks[this.hand].intersection; - var intersectionMat = new Xform(zeroVector, intersection); - var modelMat = new Xform(targetProps.rotation, targetProps.position); - var modelMatInv = modelMat.inv(); - var xformMat = Xform.mul(modelMatInv, intersectionMat); - var offsetMat = Mat4.createFromRotAndTrans(xformMat.rot, xformMat.pos); - return offsetMat; + var targetProps = Entities.getEntityProperties(this.targetObject.entityID, + [ "position", "rotation", "registrationPoint", "dimensions" ]); + return worldPositionToRegistrationFrameMatrix(targetProps, controllerData.rayPicks[this.hand].intersection); } return undefined; }; diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntityDynOnly.js b/scripts/system/controllers/controllerModules/farActionGrabEntityDynOnly.js index a080e75325..78abcb9b20 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntityDynOnly.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntityDynOnly.js @@ -12,7 +12,7 @@ makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, Picks, makeLaserLockInfo, Xform, makeLaserParams, AddressManager, getEntityParents, Selection, DISPATCHER_HOVERING_LIST, - Uuid + Uuid, worldPositionToRegistrationFrameMatrix */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -572,18 +572,9 @@ Script.include("/~/system/libraries/Xform.js"); this.calculateOffset = function(controllerData) { if (this.distanceHolding || this.distanceRotating) { - var targetProps = Entities.getEntityProperties(this.targetObject.entityID, [ - "position", - "rotation" - ]); - var zeroVector = { x: 0, y: 0, z:0, w: 0 }; - var intersection = controllerData.rayPicks[this.hand].intersection; - var intersectionMat = new Xform(zeroVector, intersection); - var modelMat = new Xform(targetProps.rotation, targetProps.position); - var modelMatInv = modelMat.inv(); - var xformMat = Xform.mul(modelMatInv, intersectionMat); - var offsetMat = Mat4.createFromRotAndTrans(xformMat.rot, xformMat.pos); - return offsetMat; + var targetProps = Entities.getEntityProperties(this.targetObject.entityID, + [ "position", "rotation", "registrationPoint", "dimensions" ]); + return worldPositionToRegistrationFrameMatrix(targetProps, controllerData.rayPicks[this.hand].intersection); } return undefined; }; diff --git a/scripts/system/controllers/controllerModules/farParentGrabEntity.js b/scripts/system/controllers/controllerModules/farParentGrabEntity.js index 439b5e5f51..a9ec246a32 100644 --- a/scripts/system/controllers/controllerModules/farParentGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farParentGrabEntity.js @@ -11,7 +11,8 @@ Entities, enableDispatcherModule, disableDispatcherModule, entityIsGrabbable, makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, Picks, makeLaserLockInfo, Xform, makeLaserParams, AddressManager, - getEntityParents, Selection, DISPATCHER_HOVERING_LIST, unhighlightTargetEntity, Messages, Uuid, findGroupParent + getEntityParents, Selection, DISPATCHER_HOVERING_LIST, unhighlightTargetEntity, Messages, Uuid, findGroupParent, + worldPositionToRegistrationFrameMatrix */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -615,18 +616,9 @@ Script.include("/~/system/libraries/Xform.js"); this.calculateOffset = function(controllerData) { if (this.distanceHolding || this.distanceRotating) { - var targetProps = Entities.getEntityProperties(this.targetObject.entityID, [ - "position", - "rotation" - ]); - var zeroVector = { x: 0, y: 0, z:0, w: 0 }; - var intersection = controllerData.rayPicks[this.hand].intersection; - var intersectionMat = new Xform(zeroVector, intersection); - var modelMat = new Xform(targetProps.rotation, targetProps.position); - var modelMatInv = modelMat.inv(); - var xformMat = Xform.mul(modelMatInv, intersectionMat); - var offsetMat = Mat4.createFromRotAndTrans(xformMat.rot, xformMat.pos); - return offsetMat; + var targetProps = Entities.getEntityProperties(this.targetObject.entityID, + [ "position", "rotation", "registrationPoint", "dimensions" ]); + return worldPositionToRegistrationFrameMatrix(targetProps, controllerData.rayPicks[this.hand].intersection); } return undefined; }; diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index a386dcf5b4..c34fd76802 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -5,7 +5,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/* global module, Camera, HMD, MyAvatar, controllerDispatcherPlugins:true, Quat, Vec3, Overlays, Xform, +/* global module, Camera, HMD, MyAvatar, controllerDispatcherPlugins:true, Quat, Vec3, Overlays, Xform, Mat4, Selection, Uuid, MSECS_PER_SEC:true , LEFT_HAND:true, RIGHT_HAND:true, FORBIDDEN_GRAB_TYPES:true, HAPTIC_PULSE_STRENGTH:true, HAPTIC_PULSE_DURATION:true, ZERO_VEC:true, ONE_VEC:true, @@ -58,7 +58,8 @@ highlightTargetEntity:true, clearHighlightedEntities:true, unhighlightTargetEntity:true, - distanceBetweenEntityLocalPositionAndBoundingBox: true + distanceBetweenEntityLocalPositionAndBoundingBox: true, + worldPositionToRegistrationFrameMatrix: true */ MSECS_PER_SEC = 1000.0; @@ -487,6 +488,30 @@ entityIsFarGrabbedByOther = function(entityID) { return false; }; + +worldPositionToRegistrationFrameMatrix = function(wptrProps, pos) { + // get world matrix for intersection point + var intersectionMat = new Xform({ x: 0, y: 0, z:0, w: 1 }, pos); + + // calculate world matrix for registrationPoint addjusted entity + var DEFAULT_REGISTRATION_POINT = { x: 0.5, y: 0.5, z: 0.5 }; + var regRatio = Vec3.subtract(DEFAULT_REGISTRATION_POINT, wptrProps.registrationPoint); + var regOffset = Vec3.multiplyVbyV(regRatio, wptrProps.dimensions); + var regOffsetRot = Vec3.multiplyQbyV(wptrProps.rotation, regOffset); + var modelMat = new Xform(wptrProps.rotation, Vec3.sum(wptrProps.position, regOffsetRot)); + + // get inverse of model matrix + var modelMatInv = modelMat.inv(); + + // transform world intersection point into object's registrationPoint frame + var xformMat = Xform.mul(modelMatInv, intersectionMat); + + // convert to Mat4 + var offsetMat = Mat4.createFromRotAndTrans(xformMat.rot, xformMat.pos); + return offsetMat; +}; + + if (typeof module !== 'undefined') { module.exports = { makeDispatcherModuleParameters: makeDispatcherModuleParameters, @@ -508,6 +533,7 @@ if (typeof module !== 'undefined') { projectOntoEntityXYPlane: projectOntoEntityXYPlane, TRIGGER_OFF_VALUE: TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE: TRIGGER_ON_VALUE, - DISPATCHER_HOVERING_LIST: DISPATCHER_HOVERING_LIST + DISPATCHER_HOVERING_LIST: DISPATCHER_HOVERING_LIST, + worldPositionToRegistrationFrameMatrix: worldPositionToRegistrationFrameMatrix }; }