499 lines
18 KiB
C#
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
|
|
}
|
|
}
|