342 lines
12 KiB
C#
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
|
|
}
|
|
}
|