// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Linq; using UltimateXR.Avatar; using UltimateXR.Core; using UltimateXR.Core.Components.Singleton; using UltimateXR.Core.Settings; using UltimateXR.Extensions.Unity; using UnityEngine; namespace UltimateXR.Manipulation { /// /// Manager that takes care of updating all the manipulation mechanics. The manipulation system handles three main /// types of entities: /// /// /// : Components usually assigned to each hand of an and /// that are able to grab objects /// /// : Objects that can be grabbed /// : Anchors where grabbable objects can be placed /// /// public partial class UxrGrabManager : UxrSingleton { #region Public Types & Data /// /// Event called whenever a component is about to try to grab something (a hand is beginning /// to close). If it ends up grabbing something will depend on whether there is a in /// reach. /// Properties available: /// /// /// : Grabber that tried to grab. /// /// /// public event EventHandler GrabTrying; /// /// Event called whenever a component is about to grab a . /// The following properties from will contain meaningful data: /// /// /// : Object that is about to be grabbed. /// /// /// : Anchor where the object is currently placed. Null /// if it isn't placed. /// /// /// : Grabber that is about to grab the object. /// /// /// : Grab point index of the object that is about to be /// grabbed. /// /// /// : true if it is already being grabbed and /// will be grabbed with one more hand after. False if no hand is currently grabbing it. /// /// /// public event EventHandler ObjectGrabbing; /// /// Same as but called right after the object was grabbed. /// public event EventHandler ObjectGrabbed; /// /// Event called whenever a component is about to release the /// that it is holding and there is no nearby /// to place it on. /// The following properties from will contain meaningful data: /// /// /// : Object that is about to be released. /// /// /// : Anchor where the object was originally grabbed /// from. Null if it wasn't grabbed from an anchor. /// /// /// : Grabber that is about to release the object. /// /// /// : Grab point index of the object that is being /// grabbed by the . /// /// /// : true if it is already being grabbed with another hand /// that will keep holding it. False if no other hand is currently grabbing it. /// /// /// : True if it was released because another /// grabbed it, false otherwise. if /// is /// true then will tell if it was released by all hands /// (false) or if it was just released by one hand and the other(s) still keep the grab (true). /// /// /// : Velocity the object is being released with. /// /// /// : Angular velocity the object is being /// released with. /// /// /// /// /// If the object is being released on a that can hold it, it will /// generate a event instead. Whenever an object is released it will either generate /// either a Place or Release event, but not both. /// public event EventHandler ObjectReleasing; /// /// Same as but called right after the object was released. /// public event EventHandler ObjectReleased; /// /// Event called whenever a is about to be placed on an /// . /// The following properties from will contain meaningful data: /// /// /// : Object that is about to be placed. /// /// /// : Anchor where the object is about to be placed on. /// /// /// : Grabber that is placing the object. /// /// /// : Grab point index of the object that is being /// grabbed by the . /// /// /// /// /// If the object is being placed it will not generate a event. Whenever an object is /// released it will either generate either a Place or Release event, but not both. /// public event EventHandler ObjectPlacing; /// /// Same as but called right after the object was placed. /// public event EventHandler ObjectPlaced; /// /// Event called whenever a is about to be removed from an /// . /// The following properties from will contain meaningful data: /// /// /// : Object that is about to be removed. /// /// /// : Anchor where the object is currently placed. /// /// /// : Grabber that is about to remove the object by grabbing it. /// This can be null if the object is removed through code using , /// or > /// /// /// : Only if the object is being removed by grabbing it: /// Grab point index of the object that is about to be grabbed by the . /// /// /// public event EventHandler ObjectRemoving; /// /// Same as but called right after the object was removed. /// public event EventHandler ObjectRemoved; /// /// Event called whenever an being grabbed by a entered the /// valid placement range (distance) of a compatible . /// The following properties from will contain meaningful data: /// /// /// : Object that entered the valid placement range. /// /// /// : Anchor where the object can potentially be placed. /// /// /// : Grabber that is holding the object. If more than one /// grabber is holding it, it will indicate the first one to grab it. /// /// /// /// /// Only enter/leave events will be generated. To check if an object can be placed on an anchor use /// . /// /// public event EventHandler AnchorRangeEntered; /// /// Same as but when leaving the valid range. /// /// /// Only enter/leave events will be generated. To check if an object can be placed on an anchor use /// . /// /// public event EventHandler AnchorRangeLeft; /// /// Event called whenever a enters the valid grab range (distance) of a /// placed on an . /// The following properties from will contain meaningful data: /// /// /// : Object that is within reach. /// /// /// : Anchor where the object is placed. /// /// /// : Grabber that entered the valid grab range. /// /// /// : Grab point index that is within reach. /// /// /// /// /// Only enter/leave events will be generated. To check if an object can be grabbed use /// . /// /// public event EventHandler PlacedObjectRangeEntered; /// /// Same as but when leaving the valid range. /// /// /// Only enter/leave events will be generated. To check if an object can be grabbed use /// . /// /// public event EventHandler PlacedObjectRangeLeft; /// /// Gets or sets the manipulation features that are used when the manager is updated. /// public UxrManipulationFeatures Features { get; set; } = UxrManipulationFeatures.All; #endregion #region Internal Methods /// /// Updates the grab manager to the current frame. /// internal void UpdateManager() { // Initializes the variables for a manipulation frame update computation. InitializeManipulationFrame(); // Updates the grabbable objects based on manipulation logic UpdateManipulation(); if (Features.HasFlag(UxrManipulationFeatures.Affordances)) { // Updates visual feedback states (objects that can be grabbed, anchors where a grabbed object can be placed on, etc.) UpdateAffordances(); } // Perform operations that need to be done at the end of the updating process. FinalizeManipulationFrame(); } #endregion #region Unity /// /// Initializes the manager and subscribes to global events. /// protected override void Awake() { base.Awake(); UxrGrabbableObjectAnchor.GlobalEnabled += GrabbableObjectAnchor_Enabled; UxrGrabbableObjectAnchor.GlobalDisabled += GrabbableObjectAnchor_Disabled; UxrGrabbableObject.GlobalDisabled += GrabbableObject_Disabled; } /// /// Unsubscribes from global events. /// protected override void OnDestroy() { base.OnDestroy(); UxrGrabbableObjectAnchor.GlobalEnabled -= GrabbableObjectAnchor_Enabled; UxrGrabbableObjectAnchor.GlobalDisabled -= GrabbableObjectAnchor_Disabled; UxrGrabbableObject.GlobalDisabled += GrabbableObject_Disabled; } /// /// Subscribes to events. /// protected override void OnEnable() { base.OnEnable(); UxrAvatar.GlobalAvatarMoved += UxrAvatar_GlobalAvatarMoved; } /// /// Unsubscribes from events. /// protected override void OnDisable() { base.OnDisable(); UxrAvatar.GlobalAvatarMoved -= UxrAvatar_GlobalAvatarMoved; } #endregion #region Event Handling Methods /// /// Called when a grabbable object anchor was enabled. Adds it to the internal list. /// /// Anchor that was enabled private void GrabbableObjectAnchor_Enabled(UxrGrabbableObjectAnchor anchor) { _grabbableObjectAnchors.Add(anchor, new GrabbableObjectAnchorInfo()); } /// /// Called when a grabbable object anchor was disabled. Removes it from the internal list. /// /// Anchor that was disabled private void GrabbableObjectAnchor_Disabled(UxrGrabbableObjectAnchor anchor) { _grabbableObjectAnchors.Remove(anchor); } /// /// Called when a grabbable object was disabled. Removes it from current grabs if present. /// /// Grabbable object that was disabled private void GrabbableObject_Disabled(UxrGrabbableObject grabbableObject) { if (_currentManipulations.ContainsKey(grabbableObject)) { _currentManipulations.Remove(grabbableObject); } } /// /// Called when an avatar was moved due to regular movement or teleportation. It is used to process the objects that /// are being grabbed to the avatar to keep it in the same relative position/orientation. /// /// Event sender /// Event parameters private void UxrAvatar_GlobalAvatarMoved(object sender, UxrAvatarMoveEventArgs e) { UxrAvatar avatar = sender as UxrAvatar; if (avatar == null || avatar.AvatarMode == UxrAvatarMode.UpdateExternally) { return; } // Create anonymous pairs of grabbable objects and their grabs that are affected by the avatar position change var dependencies = _currentManipulations.Where(pair => pair.Value.Grabbers.Any(g => g.Avatar == avatar)).Select(pair => new { GrabbableObject = pair.Key, Grabs = pair.Value.Grabs.Where(g => g.Grabber.Avatar == avatar) }); foreach (var dependency in dependencies) { UxrGrabbableObject grabbableObject = dependency.GrabbableObject; // Move grabbed objects without being parented to avatar to new position/orientation to avoid rubber-band effects if (!grabbableObject.transform.HasParent(avatar.transform)) { UxrGrabbableObject grabbableRoot = grabbableObject.AllParents.LastOrDefault() ?? grabbableObject; // Use this handy method to make the grabbable object keep the relative positioning to the avatar e.ReorientRelativeToAvatar(grabbableRoot.transform); grabbableRoot.LocalPositionBeforeUpdate = grabbableRoot.transform.localPosition; grabbableRoot.LocalRotationBeforeUpdate = grabbableRoot.transform.localRotation; ConstrainTransform(grabbableRoot); KeepGripsInPlace(grabbableRoot); foreach (UxrGrabbableObject grabbableChild in grabbableRoot.AllChildren) { grabbableChild.LocalPositionBeforeUpdate = grabbableChild.transform.localPosition; grabbableChild.LocalRotationBeforeUpdate = grabbableChild.transform.localRotation; ConstrainTransform(grabbableChild); KeepGripsInPlace(grabbableChild); } } } } #endregion #region Event Trigger Methods /// /// Event trigger for . /// /// Event parameters /// Whether to propagate the event private void OnGrabTrying(UxrManipulationEventArgs e, bool propagateEvent) { if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Verbose) { Debug.Log($"{UxrConstants.ManipulationModule} Trying to grab using {e.Grabber}."); } if (propagateEvent) { GrabTrying?.Invoke(this, e); } } /// /// Event trigger for . /// /// Event parameters /// Whether to propagate the event private void OnObjectGrabbing(UxrManipulationEventArgs e, bool propagateEvent) { if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Relevant) { Debug.Log($"{UxrConstants.ManipulationModule} {e.ToString(UxrGlobalSettings.Instance.LogLevelManipulation == UxrLogLevel.Verbose)}"); } if (propagateEvent) { ObjectGrabbing?.Invoke(this, e); } } /// /// Event trigger for . /// /// Event parameters /// Whether to propagate the event private void OnObjectGrabbed(UxrManipulationEventArgs e, bool propagateEvent) { if (propagateEvent) { ObjectGrabbed?.Invoke(this, e); } if (e.GrabbableObject) { e.GrabbableObject.UpdateGrabbableDependencies(); } } /// /// Event trigger for . /// /// Event parameters /// Whether to propagate the event private void OnObjectReleasing(UxrManipulationEventArgs e, bool propagateEvent) { if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Relevant) { Debug.Log($"{UxrConstants.ManipulationModule} {e.ToString(UxrGlobalSettings.Instance.LogLevelManipulation == UxrLogLevel.Verbose)}"); } if (propagateEvent) { ObjectReleasing?.Invoke(this, e); } } /// /// Event trigger for . /// /// Event parameters /// Whether to propagate the event private void OnObjectReleased(UxrManipulationEventArgs e, bool propagateEvent) { if (propagateEvent) { ObjectReleased?.Invoke(this, e); } if (e.GrabbableObject) { e.GrabbableObject.UpdateGrabbableDependencies(); } } /// /// Event trigger for . /// /// Event parameters /// Whether to propagate the event private void OnObjectPlacing(UxrManipulationEventArgs e, bool propagateEvent) { if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Relevant) { Debug.Log($"{UxrConstants.ManipulationModule} {e.ToString(UxrGlobalSettings.Instance.LogLevelManipulation == UxrLogLevel.Verbose)}"); } if (propagateEvent) { ObjectPlacing?.Invoke(this, e); } } /// /// Event trigger for . /// /// Event parameters /// Whether to propagate the event private void OnObjectPlaced(UxrManipulationEventArgs e, bool propagateEvent) { if (propagateEvent) { ObjectPlaced?.Invoke(this, e); } if (e.GrabbableObject) { e.GrabbableObject.UpdateGrabbableDependencies(); } } /// /// Event trigger for . /// /// Event parameters /// Whether to propagate the event private void OnObjectRemoving(UxrManipulationEventArgs e, bool propagateEvent) { if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Relevant) { Debug.Log($"{UxrConstants.ManipulationModule} {e.ToString(UxrGlobalSettings.Instance.LogLevelManipulation == UxrLogLevel.Verbose)}"); } if (propagateEvent) { ObjectRemoving?.Invoke(this, e); } } /// /// Event trigger for . /// /// Event parameters /// Whether to propagate the event private void OnObjectRemoved(UxrManipulationEventArgs e, bool propagateEvent) { if (propagateEvent) { ObjectRemoved?.Invoke(this, e); } } /// /// Event trigger for . /// /// Event parameters /// Whether to propagate the event private void OnAnchorRangeEntered(UxrManipulationEventArgs e, bool propagateEvent) { if (propagateEvent) { AnchorRangeEntered?.Invoke(this, e); } } /// /// Event trigger for . /// /// Event parameters /// Whether to propagate the event private void OnAnchorRangeLeft(UxrManipulationEventArgs e, bool propagateEvent) { if (propagateEvent) { AnchorRangeLeft?.Invoke(this, e); } } /// /// Event trigger for . /// /// Event parameters /// Whether to propagate the event private void OnPlacedObjectRangeEntered(UxrManipulationEventArgs e, bool propagateEvent) { if (propagateEvent) { PlacedObjectRangeEntered?.Invoke(this, e); } } /// /// Event trigger for . /// /// Event parameters /// Whether to propagate the event private void OnPlacedObjectRangeLeft(UxrManipulationEventArgs e, bool propagateEvent) { if (propagateEvent) { PlacedObjectRangeLeft?.Invoke(this, e); } } #endregion #region Private Methods /// /// Initializes the variables for a manipulation frame update computation. /// private void InitializeManipulationFrame() { // Store the unprocessed grabber positions for this update. foreach (UxrGrabber grabber in UxrGrabber.AllComponents) { grabber.UnprocessedGrabberPosition = grabber.transform.position; grabber.UnprocessedGrabberRotation = grabber.transform.rotation; } // Update grabbable object information void InitializeGrabbableData(UxrGrabbableObject grabbableObject) { grabbableObject.DirectLookAtChildProcessedCount = 0; grabbableObject.DirectLookAtChildGrabbedCount = grabbableObject.DirectChildrenLookAts.Count(IsBeingGrabbed); grabbableObject.LocalPositionBeforeUpdate = grabbableObject.transform.localPosition; grabbableObject.LocalRotationBeforeUpdate = grabbableObject.transform.localRotation; } foreach (KeyValuePair manipulationInfoPair in _currentManipulations) { if (manipulationInfoPair.Key == null) { continue; } UxrGrabbableObject grabbableParent = manipulationInfoPair.Key.GrabbableParent; InitializeGrabbableData(manipulationInfoPair.Key); if (grabbableParent != null) { InitializeGrabbableData(grabbableParent); } foreach (UxrGrabbableObject child in manipulationInfoPair.Key.DirectChildrenLookAts) { InitializeGrabbableData(child); } manipulationInfoPair.Value.LocalManipulationRotationPivot = Vector3.zero; } // Initialize some anchor variables for later foreach (KeyValuePair anchorPair in _grabbableObjectAnchors) { if (anchorPair.Key.CurrentPlacedObject == null) { anchorPair.Value.HadCompatibleObjectNearLastFrame = anchorPair.Value.HasCompatibleObjectNear; anchorPair.Value.HasCompatibleObjectNear = false; } else { anchorPair.Value.GrabberNear = null; } anchorPair.Value.FullGrabberNear = null; anchorPair.Value.GrabPointNear = -1; } } /// /// Performs operations that require to be done at the end of the manipulation update pipeline. /// private void FinalizeManipulationFrame() { // Update grabbers foreach (UxrGrabber grabber in UxrGrabber.EnabledComponents) { grabber.UpdateThrowPhysicsInfo(); grabber.UpdateHandGrabberRenderer(); } } /// /// Updates visual feedback states (objects that can be grabbed, anchors where a grabbed object can be placed on, /// etc.). /// private void UpdateAffordances() { // Look for grabbed objects that can be placed on anchors foreach (KeyValuePair manipulationInfoPair in _currentManipulations) { UxrGrabbableObjectAnchor anchorTargetCandidate = null; float minDistance = float.MaxValue; if (manipulationInfoPair.Key.UsesGrabbableParentDependency == false && manipulationInfoPair.Key.IsPlaceable) { foreach (KeyValuePair anchorPair in _grabbableObjectAnchors) { if (manipulationInfoPair.Key.CanBePlacedOnAnchor(anchorPair.Key, out float distance) && distance < minDistance) { anchorTargetCandidate = anchorPair.Key; minDistance = distance; } } } // Is there a compatible anchor if we would release it? store the grabber for later if (anchorTargetCandidate != null) { _grabbableObjectAnchors[anchorTargetCandidate].HasCompatibleObjectNear = true; _grabbableObjectAnchors[anchorTargetCandidate].FullGrabberNear = manipulationInfoPair.Value.Grabs[0].Grabber; _grabbableObjectAnchors[anchorTargetCandidate].LastFullGrabberNear = manipulationInfoPair.Value.Grabs[0].Grabber; } } // Look for objects that can be grabbed to update feedback objects (blinks, labels...). // First pass: get closest candidate for each grabber. Dictionary> possibleGrabs = null; foreach (UxrGrabber grabber in UxrGrabber.EnabledComponents) { if (grabber.GrabbedObject == null) { if (GetClosestGrabbableObject(grabber, out UxrGrabbableObject grabbableCandidate, out int grabPointCandidate) && !IsBeingGrabbed(grabbableCandidate, grabPointCandidate)) { if (possibleGrabs == null) { possibleGrabs = new Dictionary>(); } if (possibleGrabs.ContainsKey(grabbableCandidate)) { possibleGrabs[grabbableCandidate].Add(grabPointCandidate); } else { possibleGrabs.Add(grabbableCandidate, new List { grabPointCandidate }); } } } } // Second pass: update visual feedback objects for grabbable objects. foreach (UxrGrabbableObject grabbable in UxrGrabbableObject.EnabledComponents) { // First disable all needed, then enable them in another pass because some points may share the same object for (int point = 0; point < grabbable.GrabPointCount; ++point) { GameObject enableOnHandNear = grabbable.GetGrabPoint(point).EnableOnHandNear; if (enableOnHandNear) { bool enableObject = false; List grabPoints = null; if (possibleGrabs != null && possibleGrabs.TryGetValue(grabbable, out grabPoints)) { enableObject = grabPoints.Contains(point); } if (!enableObject && enableOnHandNear.activeSelf) { // Try to find first if other point needs to enable it bool foundEnable = false; for (int pointOther = 0; pointOther < grabbable.GrabPointCount; ++pointOther) { GameObject enableOnHandNearOther = grabbable.GetGrabPoint(pointOther).EnableOnHandNear; if (enableOnHandNear == enableOnHandNearOther) { if (possibleGrabs != null && possibleGrabs.TryGetValue(grabbable, out List grabPointsOther)) { foundEnable = grabPoints.Contains(pointOther); if (foundEnable) { break; } } } } if (!foundEnable) { enableOnHandNear.SetActive(false); break; } } } } for (int point = 0; point < grabbable.GrabPointCount; ++point) { GameObject enableOnHandNear = grabbable.GetGrabPoint(point).EnableOnHandNear; if (enableOnHandNear) { bool enableObject = false; if (possibleGrabs != null && possibleGrabs.TryGetValue(grabbable, out List grabPoints)) { enableObject = grabPoints.Contains(point); } if (enableObject && !enableOnHandNear.activeSelf) { enableOnHandNear.SetActive(true); break; } } } } // Look for empty hand being able to grab something from an anchor to update anchor visual feedback objects later and also raise events. First pass: gather info. foreach (UxrGrabber grabber in UxrGrabber.EnabledComponents) { if (grabber.GrabbedObject == null) { UxrGrabbableObjectAnchor anchorCandidate = null; int grabPointCandidate = 0; int maxPriority = int.MinValue; float minDistanceWithoutRotation = float.MaxValue; // Between different objects we don't take orientations into account foreach (KeyValuePair anchorPair in _grabbableObjectAnchors) { UxrGrabbableObjectAnchor grabbableAnchor = anchorPair.Key; if (grabbableAnchor.CurrentPlacedObject != null) { // For the same object we will not just consider the distance but also how close the grabber is to the grip orientation float minDistance = float.MaxValue; for (int point = 0; point < grabbableAnchor.CurrentPlacedObject.GrabPointCount; ++point) { if (grabbableAnchor.CurrentPlacedObject.CanBeGrabbedByGrabber(grabber, point)) { grabbableAnchor.CurrentPlacedObject.GetDistanceFromGrabber(grabber, point, out float distance, out float distanceWithoutRotation); if (grabbableAnchor.CurrentPlacedObject.Priority > maxPriority) { anchorCandidate = grabbableAnchor; grabPointCandidate = point; minDistance = distance; minDistanceWithoutRotation = distanceWithoutRotation; maxPriority = grabbableAnchor.CurrentPlacedObject.Priority; } else { if ((anchorCandidate == grabbableAnchor && distance < minDistance) || (anchorCandidate != grabbableAnchor && distanceWithoutRotation < minDistanceWithoutRotation)) { anchorCandidate = grabbableAnchor; grabPointCandidate = point; minDistance = distance; minDistanceWithoutRotation = distanceWithoutRotation; } } } } } } if (anchorCandidate != null) { _grabbableObjectAnchors[anchorCandidate].GrabberNear = null; _grabbableObjectAnchors[anchorCandidate].GrabPointNear = grabPointCandidate; } } } // Second pass: update object states and raise events. foreach (KeyValuePair anchorPair in _grabbableObjectAnchors) { if (anchorPair.Key.CurrentPlacedObject == null) { if (anchorPair.Value.LastValidGrabberNear != null) { OnPlacedObjectRangeEntered(UxrManipulationEventArgs.FromOther(UxrManipulationEventType.PlacedObjectRangeEntered, anchorPair.Key.CurrentPlacedObject, anchorPair.Key, anchorPair.Value.LastValidGrabberNear, anchorPair.Value.LastValidGrabPointNear), true); anchorPair.Value.LastValidGrabberNear = null; anchorPair.Value.LastValidGrabPointNear = -1; } if (anchorPair.Value.HasCompatibleObjectNear && !anchorPair.Value.HadCompatibleObjectNearLastFrame) { OnAnchorRangeEntered(UxrManipulationEventArgs.FromOther(UxrManipulationEventType.AnchorRangeEntered, anchorPair.Value.FullGrabberNear.GrabbedObject, anchorPair.Key, anchorPair.Value.FullGrabberNear), true); } if (!anchorPair.Value.HasCompatibleObjectNear && anchorPair.Value.HadCompatibleObjectNearLastFrame) { OnAnchorRangeLeft(UxrManipulationEventArgs.FromOther(UxrManipulationEventType.AnchorRangeLeft, anchorPair.Value.LastFullGrabberNear.GrabbedObject, anchorPair.Key, anchorPair.Value.LastFullGrabberNear), true); } if (anchorPair.Key.ActivateOnCompatibleNear) { anchorPair.Key.ActivateOnCompatibleNear.SetActive(anchorPair.Value.HasCompatibleObjectNear); } if (anchorPair.Key.ActivateOnCompatibleNotNear) { anchorPair.Key.ActivateOnCompatibleNotNear.SetActive(!anchorPair.Value.HasCompatibleObjectNear); } if (anchorPair.Key.ActivateOnHandNearAndGrabbable) { anchorPair.Key.ActivateOnHandNearAndGrabbable.SetActive(false); } } else { if (anchorPair.Value.GrabberNear != anchorPair.Value.LastValidGrabberNear) { if (anchorPair.Value.GrabberNear != null) { OnPlacedObjectRangeEntered(UxrManipulationEventArgs.FromOther(UxrManipulationEventType.PlacedObjectRangeEntered, anchorPair.Key.CurrentPlacedObject, anchorPair.Key, anchorPair.Value.GrabberNear, anchorPair.Value.GrabPointNear), true); } else if (anchorPair.Value.LastValidGrabberNear != null) { OnPlacedObjectRangeLeft(UxrManipulationEventArgs.FromOther(UxrManipulationEventType.PlacedObjectRangeLeft, anchorPair.Key.CurrentPlacedObject, anchorPair.Key, anchorPair.Value.LastValidGrabberNear, anchorPair.Value.GrabPointNear), true); } anchorPair.Value.LastValidGrabberNear = anchorPair.Value.GrabberNear; anchorPair.Value.LastValidGrabPointNear = anchorPair.Value.GrabPointNear; } if (anchorPair.Key.ActivateOnHandNearAndGrabbable) { anchorPair.Key.ActivateOnHandNearAndGrabbable.SetActive(anchorPair.Value.GrabberNear != null); } if (anchorPair.Key.ActivateOnPlaced) { anchorPair.Key.ActivateOnPlaced.SetActive(true); } if (anchorPair.Key.ActivateOnEmpty) { anchorPair.Key.ActivateOnEmpty.SetActive(false); } } } } #endregion #region Private Types & Data private Dictionary _currentManipulations = new Dictionary(); private readonly Dictionary _grabbableObjectAnchors = new Dictionary(); #endregion } }