// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.Input; using Microsoft.MixedReality.Toolkit.UI; using UnityEngine; namespace Microsoft.MixedReality.Toolkit.Experimental.UI { /// /// Simulates a point mass on a spring that can be used as a grabbable handle. /// [RequireComponent(typeof(NearInteractionGrabbable))] public class PinchSpring : MonoBehaviour, IMixedRealityPointerHandler, IMixedRealityFocusHandler { #region Serialized Fields and Properties [Experimental, SerializeField, Tooltip("The static anchor point of the spring")] private Transform handleAnchor = null; /// /// The static anchor point of the spring. /// public Transform HandleAnchor { get => handleAnchor; set => handleAnchor = value; } [SerializeField, Tooltip("The visuals to connect the anchor to the tip. This object will be scaled along the local z-axis.")] private Transform handleConnector = null; /// /// The visuals to connect the anchor to the tip. This object will be scaled along the local z-axis. /// public Transform HandleConnector { get => handleConnector; set => handleConnector = value; } [SerializeField, Tooltip("The object that acts as the point mass in the spring system.")] private Transform handleTip = null; /// /// The object that acts as the point mass in the spring system. /// public Transform HandleTip { get => handleTip; set { handleTip = value; if (handleTip != null) { tipScaleConstraint = handleTip.GetComponent(); } } } [SerializeField, Tooltip("How far the tip should be positioned from the anchor when the spring is at rest.")] private float restingDistance = 0.0f; /// /// How far the tip should rest from the anchor when the spring is at rest. /// public float RestingDistance { get => restingDistance; set => restingDistance = value; } [SerializeField, Tooltip("How far the tip should be positioned from the anchor when the spring is at rest and focused.")] private float restingFocusedDistance = 0.05f; /// /// How far the tip should be positioned from the anchor when the spring is at rest and focused. /// public float RestingFocusedDistance { get => restingFocusedDistance; set => restingFocusedDistance = value; } [SerializeField, Tooltip("The direction the tip should be positioned from the anchor when the spring is at rest.")] private Vector3 restingDirection = new Vector3(0.0f, 0.0f, 1.0f); /// /// The direction the tip should be positioned from the anchor when the spring is at rest. /// public Vector3 RestingDirection { get => restingDirection; set { restingDirection = value; restingDirection.Normalize(); } } [SerializeField, Tooltip("The mass (in kilograms) of the tip.")] private float tipMass = 0.05f; /// /// The mass (in kilograms) of the tip. /// public float TipMass { get => tipMass; set => tipMass = value; } [SerializeField, Tooltip("The constant factor characteristic of the spring, or stiffness.")] private float springStiffness = 50.0f; /// /// The constant factor characteristic of the spring, or stiffness. /// public float SpringStiffness { get => springStiffness; set => springStiffness = value; } [SerializeField, Range(0.0f, 1.0f), Tooltip("The percentage of velocity to remove from the point mass each frame.")] private float springDampening = 0.9f; /// /// The percentage of velocity to remove from the point mass each frame. /// public float SpringDampening { get => springDampening; set => springDampening = value; } [SerializeField, Tooltip("Distance (in meters) to switch from interpolation to snapping to the grasp point when being manipulated.")] private float snapDistance = 0.03f; /// /// Distance (in meters) to switch from interpolation to snapping to the grasp point when being manipulated. /// public float SnapDistance { get => snapDistance; set => snapDistance = value; } [SerializeField, Tooltip("How quickly to move the tip when interpolation the position and scale.")] private float handleTipInterpolateSpeed = 20.0f; /// /// How quickly to move the tip when interpolation the position and scale. /// public float HandleTipInterpolateSpeed { get => handleTipInterpolateSpeed; set => handleTipInterpolateSpeed = value; } #endregion #region Private Members private MinMaxScaleConstraint tipScaleConstraint = null; private IMixedRealityPointer manipulatePointer = null; private IMixedRealityPointer focusedPointer = null; private Vector3 velocity = Vector3.zero; private float currentRestingDistance = 0.0f; private bool snapped = false; #endregion #region MonoBehaviour Implementation private void Start() { restingDirection.Normalize(); if (handleTip != null) { tipScaleConstraint = handleTip.GetComponent(); } } private void FixedUpdate() { UpdateTip(); UpdateConnector(); } #endregion #region IMixedRealityFocusHandler Implementation /// public void OnPointerDown(MixedRealityPointerEventData eventData) { manipulatePointer = eventData.Pointer; // Continue to forward the events upward because this component is a passive observer. EventSystemExtensions.ExecuteHierarchyUpward(gameObject, eventData, MixedRealityInputSystem.OnPointerDownEventHandler); } /// public void OnPointerDragged(MixedRealityPointerEventData eventData) { // Continue to forward the events upward because this component is a passive observer. EventSystemExtensions.ExecuteHierarchyUpward(gameObject, eventData, MixedRealityInputSystem.OnPointerDraggedEventHandler); } /// public void OnPointerUp(MixedRealityPointerEventData eventData) { manipulatePointer = null; // Continue to forward the events upward because this component is a passive observer. EventSystemExtensions.ExecuteHierarchyUpward(gameObject, eventData, MixedRealityInputSystem.OnPointerUpEventHandler); } /// public void OnPointerClicked(MixedRealityPointerEventData eventData) { // Continue to forward the events upward because this component is a passive observer. EventSystemExtensions.ExecuteHierarchyUpward(gameObject, eventData, MixedRealityInputSystem.OnInputClickedEventHandler); } #endregion #region IMixedRealityFocusHandler Implementation /// public void OnFocusEnter(FocusEventData eventData) { focusedPointer = eventData.Pointer; // Continue to forward the events upward because this component is a passive observer. EventSystemExtensions.ExecuteHierarchyUpward(gameObject, eventData, MixedRealityInputSystem.OnFocusEnterEventHandler); } /// public void OnFocusExit(FocusEventData eventData) { focusedPointer = null; // Continue to forward the events upward because this component is a passive observer. EventSystemExtensions.ExecuteHierarchyUpward(gameObject, eventData, MixedRealityInputSystem.OnFocusExitEventHandler); } #endregion #region Private Methods private void UpdateTip() { var deltaTime = Time.deltaTime; var t = handleTipInterpolateSpeed * deltaTime; // Move the handle tip towards the interacting pointer, else spring back to the resting location. var currentPosition = handleTip.position; var isFocused = focusedPointer != null; currentRestingDistance = Mathf.Lerp(currentRestingDistance, isFocused ? restingFocusedDistance : restingDistance, t); var restingPosition = handleAnchor.position + (transform.rotation * restingDirection) * currentRestingDistance; Vector3 targetPosition; Vector3 graspPosition; var nearPointer = focusedPointer as IMixedRealityNearPointer; if (nearPointer != null && nearPointer.TryGetNearGraspPoint(out graspPosition)) { if (snapped || ((currentPosition - graspPosition).magnitude < snapDistance)) { // Snap to the grasp position. targetPosition = graspPosition; snapped = true; } else { // Interpolate to the snap position. targetPosition = Vector3.Lerp(currentPosition, graspPosition, t); } } else { // Spring back to the resting position using Hooke's Law. var delta = restingPosition - currentPosition; if (delta != Vector3.zero) { var deltaMagnitude = delta.magnitude; delta /= deltaMagnitude; // Integrate the point mass. var force = delta * (springStiffness * deltaMagnitude); var acceleration = force / tipMass; velocity += acceleration * deltaTime; targetPosition = currentPosition + (velocity * deltaTime); velocity *= springDampening; } else { targetPosition = restingPosition; } snapped = false; } handleTip.position = targetPosition; // Update the tip scale. if (tipScaleConstraint != null) { var isManipulated = manipulatePointer != null; var tagetScale = isManipulated ? Vector3.one * tipScaleConstraint.ScaleMinimum : Vector3.one * tipScaleConstraint.ScaleMaximum; handleTip.localScale = Vector3.Lerp(handleTip.localScale, tagetScale, t); } } private void UpdateConnector() { if (handleConnector != null) { var delta = handleTip.position - handleAnchor.position; handleConnector.position = (handleAnchor.position + handleTip.position) * 0.5f; if (delta != Vector3.zero) { handleConnector.rotation = Quaternion.LookRotation(delta); } var scale = handleConnector.localScale; handleConnector.localScale = new Vector3(scale.x, scale.y, delta.magnitude); } } #endregion } }