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

499 lines
18 KiB
C#

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using Microsoft.MixedReality.Toolkit.Input;
using Microsoft.MixedReality.Toolkit.UI;
using Microsoft.MixedReality.Toolkit.Utilities;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Experimental.UI
{
/// <summary>
/// Manages and creates sliders to allow for non-uniform scaling of a box along multiple axes.
/// The <see cref="MinMaxScaleConstraint"/> is
/// used to control the target of the scale and scale constraints.
/// </summary>
[RequireComponent(typeof(MinMaxScaleConstraint))]
public class PinchSliderBox : MonoBehaviour
{
#region Serialized Fields and Properties
[Experimental, SerializeField, Tooltip("Should sliders be auto-created when this component is enabled?")]
private bool createSlidersOnEnable = true;
/// <summary>
/// Should sliders be auto-created when this component is enabled?
/// </summary>
public bool CreateSlidersOnEnable
{
get => createSlidersOnEnable;
set => createSlidersOnEnable = value;
}
[SerializeField, Tooltip("Should sliders be created for manipulating scale on the 'X' axis?")]
private bool createXAxisSliders = true;
/// <summary>
/// Should sliders be created for manipulating scale on the 'X' axis?
/// </summary>
public bool CreateXAxisSliders
{
get => createXAxisSliders;
set
{
createXAxisSliders = value;
CreateSliders();
}
}
[SerializeField, Tooltip("Should sliders be created for manipulating scale on the 'Y' axis?")]
private bool createYAxisSliders = true;
/// <summary>
/// Should sliders be created for manipulating scale on the 'Y' axis?
/// </summary>
public bool CreateYAxisSliders
{
get => createYAxisSliders;
set
{
createYAxisSliders = value;
CreateSliders();
}
}
[SerializeField, Tooltip("Should sliders be created for manipulating scale on the 'Z' axis?")]
private bool createZAxisSliders = true;
/// <summary>
/// Should sliders be created for manipulating scale on the 'Z' axis?
/// </summary>
public bool CreateZAxisSliders
{
get => createZAxisSliders;
set
{
createZAxisSliders = value;
CreateSliders();
}
}
[SerializeField, Tooltip("The prefab to spawn for slider thumb visualization.")]
private GameObject thumbPrefab = null;
/// <summary>
/// The prefab to spawn for slider thumb visualization.
/// </summary>
public GameObject ThumbPrefab
{
get => thumbPrefab;
set
{
thumbPrefab = value;
CreateSliders();
}
}
[SerializeField, Tooltip("The prefab use to demonstrate which axis of the box is being manipulated.")]
private GameObject hightlightPrefab = null;
/// <summary>
/// The prefab use to demonstrate which axis of the box is being manipulated.
/// </summary>
public GameObject HightlightPrefab
{
get => hightlightPrefab;
set
{
hightlightPrefab = value;
CreateSliders();
}
}
#endregion
#region Private Members
private MinMaxScaleConstraint scaleConstraint = null;
private Transform pivot = null;
private Transform axisHighlight = null;
private Material defaultThumbMaterial = null;
private bool quitting = false;
private const int PositiveIndex = 0;
private const int NegativeIndex = 1;
private const int AxisOffset = 2;
private class SliderPair
{
public PinchSlider[] Sliders = new PinchSlider[2];
public float Value
{
get
{
return (Sliders[PositiveIndex].SliderValue +
Sliders[NegativeIndex].SliderValue) * 0.5f;
}
}
}
private class SliderPlane
{
public SliderAxis Axis = SliderAxis.XAxis;
public SliderPair[] SliderPairs = new SliderPair[2];
public SliderPair GetSliderPair(PinchSlider slider)
{
if (slider == SliderPairs[PositiveIndex].Sliders[PositiveIndex] ||
slider == SliderPairs[PositiveIndex].Sliders[NegativeIndex])
{
return SliderPairs[PositiveIndex];
}
else
{
return SliderPairs[NegativeIndex];
}
}
}
private const int SliderPlaneCount = 3;
private SliderPlane[] sliderPlanes = new SliderPlane[SliderPlaneCount];
private Dictionary<PinchSlider, SliderPlane> sliderToPlane = new Dictionary<PinchSlider, SliderPlane>();
#endregion
#region MonoBehaviour Implementation
private void Awake()
{
// Ensure a [MinMaxScaleConstraint](xref:Microsoft.MixedReality.Toolkit.UI.MinMaxScaleConstraint) exists and it is initialized.
scaleConstraint = gameObject.EnsureComponent<MinMaxScaleConstraint>();
scaleConstraint.Initialize(new MixedRealityTransform(transform));
if (thumbPrefab == null)
{
defaultThumbMaterial = new Material(StandardShaderUtility.MrtkStandardShader);
}
}
private void OnDestroy()
{
Destroy(defaultThumbMaterial);
}
private void OnEnable()
{
if (createSlidersOnEnable)
{
CreateSliders();
}
}
private void OnDisable()
{
OnHoverExited(null);
DestroyHandles();
}
private void OnApplicationQuit()
{
quitting = true;
}
#endregion
#region Public Methods
/// <summary>
/// Creates sliders for each requested axis of the box. A pivot object is also created and
/// made the parent of the <see cref="MinMaxScaleConstraint"/> TargetTransform.
/// </summary>
[ContextMenu("Create Sliders")]
public void CreateSliders()
{
DestroyHandles();
// Create a pivot object to contain the sliders and aide in non-uniform scaling.
pivot = new GameObject($"{nameof(PinchSliderBox)}Pivot").transform;
pivot.parent = scaleConstraint.transform.parent;
pivot.localPosition = scaleConstraint.transform.localPosition;
pivot.localRotation = scaleConstraint.transform.localRotation;
scaleConstraint.transform.parent = pivot;
// Create an axis highlight game object to toggle when sliders are hovered upon.
if (hightlightPrefab != null)
{
axisHighlight = Instantiate(hightlightPrefab, pivot, false).transform;
axisHighlight.gameObject.SetActive(false);
}
// Create the requested sliders.
if (createXAxisSliders)
{
sliderPlanes[(int)SliderAxis.XAxis] = AddSliderPlane(SliderAxis.XAxis);
}
if (createYAxisSliders)
{
sliderPlanes[(int)SliderAxis.YAxis] = AddSliderPlane(SliderAxis.YAxis);
}
if (createZAxisSliders)
{
sliderPlanes[(int)SliderAxis.ZAxis] = AddSliderPlane(SliderAxis.ZAxis);
}
}
/// <summary>
/// Destroys all sliders created with CreateSliders and restores the
/// <see cref="MinMaxScaleConstraint"/>'s TargetTransform's parent.
/// </summary>
public void DestroyHandles()
{
if (pivot != null)
{
// Restore the original parent. Unity will present a warning if parents are altered when quitting.
if (!quitting)
{
scaleConstraint.transform.parent = pivot.parent;
}
Destroy(pivot.gameObject);
pivot = null;
axisHighlight = null;
}
for (var i = 0; i < SliderPlaneCount; ++i)
{
sliderPlanes[i] = null;
}
sliderToPlane.Clear();
}
#endregion
#region Private Methods
private SliderPlane AddSliderPlane(SliderAxis axis)
{
var sliders = new PinchSlider[4];
var globalDirection = 1.0f;
for (var i = 0; i < 2; ++i)
{
var localDirection = 1.0f;
for (var j = 0; j < 2; ++j)
{
sliders[i * 2 + j] = AddSlider(axis, globalDirection, localDirection);
localDirection = -localDirection;
}
globalDirection = -globalDirection;
}
var sliderPlane = new SliderPlane()
{
Axis = axis,
SliderPairs = new SliderPair[]
{
new SliderPair() { Sliders = new PinchSlider[] { sliders[0], sliders[1] } },
new SliderPair() { Sliders = new PinchSlider[] { sliders[2], sliders[3] } }
}
};
foreach (var slider in sliders)
{
sliderToPlane.Add(slider, sliderPlane);
}
return sliderPlane;
}
private PinchSlider AddSlider(SliderAxis axis, float globalDirection, float localDirection)
{
var slider = new GameObject($"Slider {axis} {globalDirection} {localDirection}").AddComponent<PinchSlider>();
slider.transform.parent = pivot;
// Calculates a normal to the pinch slider axis to place the slider at.
var targetTransform = scaleConstraint.transform;
var axisNormal = GetSliderAxisDirection(CalculateAxisNormal(axis));
var axisNormalHalfScale = CalculateAxisHalfScale(targetTransform, axisNormal);
slider.transform.position = CalculateSliderPosition(targetTransform, axisNormal, axisNormalHalfScale, globalDirection);
slider.transform.rotation = targetTransform.rotation;
slider.CurrentSliderAxis = axis;
var axisIndex = (int)axis;
slider.SliderStartDistance = scaleConstraint.ScaleMinimumVector[axisIndex] * 0.5f * localDirection;
slider.SliderEndDistance = scaleConstraint.ScaleMaximumVector[axisIndex] * 0.5f * localDirection;
GameObject thumb;
if (thumbPrefab != null)
{
thumb = Instantiate(thumbPrefab, slider.transform, false);
thumb.EnsureComponent<NearInteractionGrabbable>();
thumb.transform.rotation = Quaternion.LookRotation((targetTransform.rotation * axisNormal) * globalDirection);
}
else
{
thumb = CreateDefaultThumb(defaultThumbMaterial, slider.transform);
}
slider.ThumbRoot = thumb;
var scaleRange = scaleConstraint.ScaleMaximumVector[axisIndex] - scaleConstraint.ScaleMinimumVector[axisIndex];
slider.SliderValue = (targetTransform.localScale[axisIndex] - scaleConstraint.ScaleMinimumVector[axisIndex]) / scaleRange;
slider.OnValueUpdated.AddListener(OnSlideValueUpdated);
slider.OnHoverEntered.AddListener(OnHoverEntered);
slider.OnHoverExited.AddListener(OnHoverExited);
return slider;
}
private static Vector3 GetSliderAxisDirection(SliderAxis sliderAxis)
{
switch (sliderAxis)
{
case SliderAxis.XAxis:
return Vector3.right;
case SliderAxis.YAxis:
return Vector3.up;
case SliderAxis.ZAxis:
return Vector3.forward;
default:
throw new ArgumentOutOfRangeException("Underhanded SliderAxis passed to GetSliderAxisDirection.");
}
}
private static SliderAxis CalculateAxisNormal(SliderAxis axis)
{
return (SliderAxis)(((int)axis + AxisOffset) % SliderPlaneCount);
}
private static float CalculateAxisHalfScale(Transform targetTransform, Vector3 axis)
{
return Vector3.Dot(axis, targetTransform.localScale) * 0.5f;
}
private static Vector3 CalculateSliderPosition(Transform targetTransform, Vector3 axisNormal, float scale, float direction)
{
return targetTransform.position + (((targetTransform.rotation * axisNormal) * scale) * direction);
}
private static GameObject CreateDefaultThumb(Material material, Transform parent)
{
var primitive = GameObject.CreatePrimitive(PrimitiveType.Sphere);
primitive.name = "Thumb";
primitive.AddComponent<NearInteractionGrabbable>();
primitive.GetComponent<Renderer>().material = material;
primitive.GetComponent<SphereCollider>().radius *= 3.0f;
primitive.transform.parent = parent;
primitive.transform.localPosition = Vector3.zero;
primitive.transform.localRotation = Quaternion.identity;
primitive.transform.localScale = Vector3.one * 0.03f;
return primitive;
}
private void OnSlideValueUpdated(SliderEventData data)
{
var sliderPlane = sliderToPlane[data.Slider];
var sliderPair = sliderPlane.GetSliderPair(data.Slider);
var axisIndex = (int)sliderPlane.Axis;
var scaleMin = scaleConstraint.ScaleMinimumVector[axisIndex];
var scaleMax = scaleConstraint.ScaleMaximumVector[axisIndex];
var scaleRange = scaleMax - scaleMin;
var targetTransform = scaleConstraint.transform;
// Update scale.
var scale = targetTransform.localScale;
scale[axisIndex] = (sliderPair.Value * scaleRange + scaleMin);
targetTransform.localScale = scale;
// Update position.
var position = targetTransform.localPosition;
position[axisIndex] = ((sliderPair.Sliders[PositiveIndex].SliderValue * scaleRange + scaleMin) * 0.25f) -
((sliderPair.Sliders[NegativeIndex].SliderValue * scaleRange + scaleMin) * 0.25f);
targetTransform.localPosition = position;
// Update the opposite slider pair.
var oppositeSliderPair = (sliderPair == sliderPlane.SliderPairs[PositiveIndex]) ? sliderPlane.SliderPairs[NegativeIndex] :
sliderPlane.SliderPairs[PositiveIndex];
for (var i = 0; i < 2; ++i)
{
if (oppositeSliderPair.Sliders[i].SliderValue != sliderPair.Sliders[i].SliderValue)
{
oppositeSliderPair.Sliders[i].SliderValue = sliderPair.Sliders[i].SliderValue;
}
}
// Update the position of sliders on the modified plane.
var copanarSliderPlane = sliderPlanes[(axisIndex + AxisOffset - 1) % SliderPlaneCount];
if (copanarSliderPlane != null)
{
var axisNormal = GetSliderAxisDirection(CalculateAxisNormal(copanarSliderPlane.Axis));
var axisNormalInverse = axisNormal;
for (var i = 0; i < 3; ++i)
{
axisNormalInverse[i] = axisNormalInverse[i] == 1.0f ? 0.0f : 1.0f;
}
var axisNormalHalfScale = CalculateAxisHalfScale(targetTransform, axisNormal);
var globalDirection = 1.0f;
for (var i = 0; i < 2; ++i)
{
for (var j = 0; j < 2; ++j)
{
var slider = copanarSliderPlane.SliderPairs[i].Sliders[j];
slider.transform.position = CalculateSliderPosition(targetTransform, axisNormal, axisNormalHalfScale, globalDirection);
// Remove any translation due to scale.
slider.transform.localPosition -= Vector3.Scale(slider.transform.localPosition, axisNormalInverse);
}
globalDirection = -globalDirection;
}
}
}
private void OnHoverEntered(SliderEventData data)
{
if (axisHighlight != null)
{
axisHighlight.gameObject.SetActive(true);
// Move the highlight to the hovered slider.
var axisType = data.Slider.CurrentSliderAxis;
var axis = GetSliderAxisDirection(axisType);
var sliderPair = sliderToPlane[data.Slider].GetSliderPair(data.Slider);
var direction = (sliderPair.Sliders[PositiveIndex] == data.Slider) ? 1.0f : -1.0f;
axisHighlight.parent = scaleConstraint.transform;
axisHighlight.localPosition = axis * 0.5f * direction;
axisHighlight.localRotation = Quaternion.LookRotation(axis, axisType == SliderAxis.YAxis ? Vector3.right : Vector3.up);
axisHighlight.localScale = Vector3.one;
}
}
private void OnHoverExited(SliderEventData data)
{
if (axisHighlight != null)
{
axisHighlight.gameObject.SetActive(false);
}
}
#endregion
}
}