mixedreality/com.microsoft.mixedreality..../SDK/Experimental/PinchSliderBox/PinchSpring.cs

342 lines
12 KiB
C#

// 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
{
/// <summary>
/// Simulates a point mass on a spring that can be used as a grabbable handle.
/// </summary>
[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;
/// <summary>
/// The static anchor point of the spring.
/// </summary>
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;
/// <summary>
/// The visuals to connect the anchor to the tip. This object will be scaled along the local z-axis.
/// </summary>
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;
/// <summary>
/// The object that acts as the point mass in the spring system.
/// </summary>
public Transform HandleTip
{
get => handleTip;
set
{
handleTip = value;
if (handleTip != null)
{
tipScaleConstraint = handleTip.GetComponent<MinMaxScaleConstraint>();
}
}
}
[SerializeField, Tooltip("How far the tip should be positioned from the anchor when the spring is at rest.")]
private float restingDistance = 0.0f;
/// <summary>
/// How far the tip should rest from the anchor when the spring is at rest.
/// </summary>
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;
/// <summary>
/// How far the tip should be positioned from the anchor when the spring is at rest and focused.
/// </summary>
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);
/// <summary>
/// The direction the tip should be positioned from the anchor when the spring is at rest.
/// </summary>
public Vector3 RestingDirection
{
get => restingDirection;
set
{
restingDirection = value;
restingDirection.Normalize();
}
}
[SerializeField, Tooltip("The mass (in kilograms) of the tip.")]
private float tipMass = 0.05f;
/// <summary>
/// The mass (in kilograms) of the tip.
/// </summary>
public float TipMass
{
get => tipMass;
set => tipMass = value;
}
[SerializeField, Tooltip("The constant factor characteristic of the spring, or stiffness.")]
private float springStiffness = 50.0f;
/// <summary>
/// The constant factor characteristic of the spring, or stiffness.
/// </summary>
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;
/// <summary>
/// The percentage of velocity to remove from the point mass each frame.
/// </summary>
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;
/// <summary>
/// Distance (in meters) to switch from interpolation to snapping to the grasp point when being manipulated.
/// </summary>
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;
/// <summary>
/// How quickly to move the tip when interpolation the position and scale.
/// </summary>
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<MinMaxScaleConstraint>();
}
}
private void FixedUpdate()
{
UpdateTip();
UpdateConnector();
}
#endregion
#region IMixedRealityFocusHandler Implementation
/// <inheritdoc />
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);
}
/// <inheritdoc />
public void OnPointerDragged(MixedRealityPointerEventData eventData)
{
// Continue to forward the events upward because this component is a passive observer.
EventSystemExtensions.ExecuteHierarchyUpward(gameObject, eventData, MixedRealityInputSystem.OnPointerDraggedEventHandler);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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
/// <inheritdoc />
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);
}
/// <inheritdoc />
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
}
}