// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- using System; using System.Collections; using UltimateXR.Avatar; using UltimateXR.Core; using UltimateXR.Core.Components; using UltimateXR.Extensions.Unity; using UnityEngine; #pragma warning disable 67 // Disable warnings due to unused events namespace UltimateXR.Manipulation.Helpers { /// /// /// Component that allows an object to be scaled by grabbing it by both sides and moving them closer or apart. /// The hierarchy should be as follows: /// /// /// -Root GameObject: With UxrGrabbableResizable and UxrGrabbableObject component. /// | The UxrGrabbableObject is a dummy grabbable parent that enables moving /// | this root by grabbing the child extensions. It can also have its own /// | grab points but they are not required. /// |---Root resizable: Object that will be scaled when the two extensions are moved. /// |---Grabbable left: Left grabbable extension with locked rotation and translation /// | constrained to sliding it left-right. /// |---Grabbable right: Right grabbable extension with locked rotation and translation /// constrained to sliding it left-right. /// /// All objects should use an axis system with x right, y up and z forward. /// public sealed partial class UxrGrabbableResizable : UxrComponent, IUxrGrabbable { #region Inspector Properties/Serialized Fields [Header("General")] [SerializeField] private Transform _resizableRoot; [SerializeField] private float _startScale = 1.0f; [Header("Grabbing")] [SerializeField] private UxrGrabbableObject _grabbableRoot; [SerializeField] private UxrGrabbableObject _grabbableExtendLeft; [SerializeField] private UxrGrabbableObject _grabbableExtendRight; [Header("Haptics")] [SerializeField] [Range(0.0f, 1.0f)] private float _hapticsIntensity = 0.1f; #endregion #region Public Types & Data /// /// Gets the that is going to be scaled when the two grabbable objects are moved apart. /// public Transform ResizableRoot => _resizableRoot; /// /// Gets the root grabbable object. /// public UxrGrabbableObject GrabbableRoot => _grabbableRoot; /// /// Gets the left grabbable extension. /// public UxrGrabbableObject GrabbableExtendLeft => _grabbableExtendLeft; /// /// Gets the right grabbable extension. /// public UxrGrabbableObject GrabbableExtendRight => _grabbableExtendRight; #endregion #region Implicit IUxrGrabbable /// public bool IsBeingGrabbed => GrabbableRoot.IsBeingGrabbed || GrabbableExtendLeft.IsBeingGrabbed || GrabbableExtendRight.IsBeingGrabbed; /// public bool IsGrabbable { get => GrabbableRoot.IsGrabbable || GrabbableExtendLeft.IsGrabbable || GrabbableExtendRight.IsGrabbable; set { BeginSync(); GrabbableRoot.IsGrabbable = value; GrabbableExtendLeft.IsGrabbable = value; GrabbableExtendRight.IsGrabbable = value; EndSyncProperty(value); } } /// public bool IsKinematic { get => GrabbableRoot.IsKinematic || GrabbableExtendLeft.IsKinematic || GrabbableExtendRight.IsKinematic; set { BeginSync(); GrabbableRoot.IsKinematic = value; GrabbableExtendLeft.IsKinematic = value; GrabbableExtendRight.IsKinematic = value; EndSyncProperty(value); } } /// public event EventHandler Grabbing; /// public event EventHandler Grabbed; /// public event EventHandler Releasing; /// public event EventHandler Released; /// public event EventHandler Placing; /// public event EventHandler Placed; /// public void ResetPositionAndState(bool propagateEvents) { // This method will be synchronized through network BeginSync(); ReleaseGrabs(true); GrabbableRoot.ResetPositionAndState(propagateEvents); GrabbableExtendLeft.ResetPositionAndState(propagateEvents); GrabbableExtendRight.ResetPositionAndState(propagateEvents); UpdateResizableScale(); EndSyncMethod(new object[] { propagateEvents }); } /// public void ReleaseGrabs(bool propagateEvents) { // This method will be synchronized through network BeginSync(); GrabbableRoot.ReleaseGrabs(propagateEvents); GrabbableExtendLeft.ReleaseGrabs(propagateEvents); GrabbableExtendRight.ReleaseGrabs(propagateEvents); _grabbingCount = 0; _grabbedCount = 0; EndSyncMethod(new object[] { propagateEvents }); } #endregion #region Unity /// /// Initializes the component. /// protected override void Awake() { base.Awake(); _initialGrabsSeparation = Vector3.Distance(GrabbableExtendLeft.transform.position, GrabbableExtendRight.transform.position); _initialResizableLocalScale = _resizableRoot.transform.localScale; _separationToBoundsFactor = _initialGrabsSeparation / _resizableRoot.gameObject.GetLocalBounds(true).size.x; } /// /// Subscribes to relevant events. /// protected override void OnEnable() { base.OnEnable(); UxrManager.AvatarsUpdated += UxrManager_AvatarsUpdated; GrabbableRoot.Grabbing += Grabbable_Grabbing; GrabbableRoot.Grabbed += Grabbable_Grabbed; GrabbableRoot.Releasing += Grabbable_Releasing; GrabbableRoot.Released += Grabbable_Released; GrabbableExtendLeft.Grabbing += Grabbable_Grabbing; GrabbableExtendLeft.Grabbed += Grabbable_Grabbed; GrabbableExtendLeft.Releasing += Grabbable_Releasing; GrabbableExtendLeft.Released += Grabbable_Released; GrabbableExtendRight.Grabbed += Grabbable_Grabbed; GrabbableExtendRight.Grabbing += Grabbable_Grabbing; GrabbableExtendRight.Releasing += Grabbable_Releasing; GrabbableExtendRight.Released += Grabbable_Released; _hapticsCoroutine = StartCoroutine(HapticsCoroutine()); } /// /// Unsubscribes from relevant events. /// protected override void OnDisable() { base.OnDisable(); UxrManager.AvatarsUpdated -= UxrManager_AvatarsUpdated; GrabbableRoot.Grabbing -= Grabbable_Grabbing; GrabbableRoot.Grabbed -= Grabbable_Grabbed; GrabbableRoot.Releasing -= Grabbable_Releasing; GrabbableRoot.Released -= Grabbable_Released; GrabbableExtendLeft.Grabbing -= Grabbable_Grabbing; GrabbableExtendLeft.Grabbed -= Grabbable_Grabbed; GrabbableExtendLeft.Releasing -= Grabbable_Releasing; GrabbableExtendLeft.Released -= Grabbable_Released; GrabbableExtendRight.Grabbed -= Grabbable_Grabbed; GrabbableExtendRight.Grabbing -= Grabbable_Grabbing; GrabbableExtendRight.Releasing -= Grabbable_Releasing; GrabbableExtendRight.Released -= Grabbable_Released; StopCoroutine(_hapticsCoroutine); } /// /// Scales the resizable using the initial scale if it's different than 1.0 /// protected override void Start() { base.Start(); if (!Mathf.Approximately(1.0f, _startScale)) { float halfOffset = (_startScale * _initialGrabsSeparation - _initialGrabsSeparation) * 0.5f; GrabbableExtendLeft.transform.localPosition -= Vector3.right * halfOffset; GrabbableExtendRight.transform.localPosition += Vector3.right * halfOffset; } } #endregion #region Coroutines /// /// Coroutine that sends haptic feedback in case of scaling. /// /// Coroutine IEnumerator private IEnumerator HapticsCoroutine() { void SendHapticClip(UxrGrabbableObject grabbableObject, UxrHandSide handSide, float speed) { if (_hapticsIntensity < 0.001f || !UxrGrabManager.Instance.GetGrabbingHand(grabbableObject, 0, out UxrGrabber grabber) || grabber.Avatar.AvatarMode != UxrAvatarMode.Local) { return; } float quantityPos = HapticsManipulationMaxSpeed - HapticsManipulationMinSpeed <= 0.0f ? 0.0f : (speed - HapticsManipulationMinSpeed) / (HapticsManipulationMaxSpeed - HapticsManipulationMinSpeed); if (quantityPos > 0.0f) { float frequencyPos = Mathf.Lerp(HapticsManipulationMinFrequency, HapticsManipulationMaxFrequency, Mathf.Clamp01(quantityPos)); float amplitudePos = Mathf.Lerp(0.1f, 1.0f, Mathf.Clamp01(quantityPos)) * _hapticsIntensity; UxrAvatar.LocalAvatarInput.SendHapticFeedback(handSide, frequencyPos, amplitudePos, UxrConstants.InputControllers.HapticSampleDurationSeconds); } } float lastDistance = Vector3.Distance(GrabbableExtendLeft.transform.position, GrabbableExtendRight.transform.position); while (true) { if (_grabbableExtendLeft != null && _grabbableExtendRight != null && _grabbableExtendLeft.IsBeingGrabbed && _grabbableExtendRight.IsBeingGrabbed) { float currentDistance = Vector3.Distance(GrabbableExtendLeft.transform.position, GrabbableExtendRight.transform.position); float speed = Mathf.Abs(lastDistance - currentDistance) / UxrConstants.InputControllers.HapticSampleDurationSeconds; SendHapticClip(_grabbableExtendLeft, UxrHandSide.Left, speed); SendHapticClip(_grabbableExtendRight, UxrHandSide.Right, speed); lastDistance = Vector3.Distance(GrabbableExtendLeft.transform.position, GrabbableExtendRight.transform.position); } yield return new WaitForSeconds(UxrConstants.InputControllers.HapticSampleDurationSeconds); } } #endregion #region Event Handling Methods /// /// Called right after the avatars and manipulation update. Scale the object at this point. /// private void UxrManager_AvatarsUpdated() { UpdateResizableScale(); } /// /// Called when any grabbable is about to be grabbed. It is responsible for sending the appropriate /// manipulation events if necessary. /// /// Event sender /// Event parameters private void Grabbable_Grabbing(object sender, UxrManipulationEventArgs e) { if (e.IsGrabbedStateChanged) { _grabbingCount++; if (_grabbingCount == 1) { Grabbing?.Invoke(this, e); } } } /// /// Called right after any grabbable was grabbed. It is responsible for sending the appropriate /// manipulation events if necessary. /// /// Event sender /// Event parameters private void Grabbable_Grabbed(object sender, UxrManipulationEventArgs e) { if (e.IsGrabbedStateChanged) { _grabbedCount++; if (_grabbedCount == 1) { Grabbed?.Invoke(this, e); } } } /// /// Called when any grabbable is about to be released. It is responsible for sending the appropriate /// manipulation events if necessary. /// /// Event sender /// Event parameters private void Grabbable_Releasing(object sender, UxrManipulationEventArgs e) { if (e.IsGrabbedStateChanged) { _grabbingCount--; if (_grabbingCount == 0) { Releasing?.Invoke(this, e); } } } /// /// Called right after any grabbable was released. It is responsible for sending the appropriate /// manipulation events if necessary. /// /// Event sender /// Event parameters private void Grabbable_Released(object sender, UxrManipulationEventArgs e) { if (e.IsGrabbedStateChanged) { _grabbedCount--; if (_grabbedCount == 0) { Released?.Invoke(this, e); } } } #endregion #region Private Methods /// /// Updates the resizable scale based on the current separation between the left and right extensions. /// private void UpdateResizableScale() { float currentGrabSeparation = Vector3.Distance(GrabbableExtendLeft.transform.position, GrabbableExtendRight.transform.position); // Move the center in between the two extensions Vector3 localCenter = transform.InverseTransformPoint((GrabbableExtendLeft.transform.position + GrabbableExtendRight.transform.position) * 0.5f); Vector3 resizableLocalPos = _resizableRoot.transform.localPosition; resizableLocalPos.x = localCenter.x; _resizableRoot.transform.localPosition = resizableLocalPos; // Scale the object float localScaleZ = _resizableRoot.transform.localScale.z; Vector3 resizableLocalScale = _initialResizableLocalScale * (currentGrabSeparation / _initialGrabsSeparation * _separationToBoundsFactor); resizableLocalScale.z = localScaleZ; _resizableRoot.transform.localScale = resizableLocalScale; } #endregion #region Private Types & Data private const float HapticsManipulationMinSpeed = 0.03f; private const float HapticsManipulationMaxSpeed = 1.0f; private const float HapticsManipulationMinFrequency = 10; private const float HapticsManipulationMaxFrequency = 100; private Vector3 _initialResizableLocalScale; private float _initialGrabsSeparation; private float _separationToBoundsFactor; private int _grabbingCount; private int _grabbedCount; private Coroutine _hapticsCoroutine; #endregion } } #pragma warning restore 67