// 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 { /// /// Manages and creates sliders to allow for non-uniform scaling of a box along multiple axes. /// The is /// used to control the target of the scale and scale constraints. /// [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; /// /// Should sliders be auto-created when this component is enabled? /// 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; /// /// Should sliders be created for manipulating scale on the 'X' axis? /// 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; /// /// Should sliders be created for manipulating scale on the 'Y' axis? /// 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; /// /// Should sliders be created for manipulating scale on the 'Z' axis? /// public bool CreateZAxisSliders { get => createZAxisSliders; set { createZAxisSliders = value; CreateSliders(); } } [SerializeField, Tooltip("The prefab to spawn for slider thumb visualization.")] private GameObject thumbPrefab = null; /// /// The prefab to spawn for slider thumb visualization. /// 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; /// /// The prefab use to demonstrate which axis of the box is being manipulated. /// 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 sliderToPlane = new Dictionary(); #endregion #region MonoBehaviour Implementation private void Awake() { // Ensure a [MinMaxScaleConstraint](xref:Microsoft.MixedReality.Toolkit.UI.MinMaxScaleConstraint) exists and it is initialized. scaleConstraint = gameObject.EnsureComponent(); 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 /// /// Creates sliders for each requested axis of the box. A pivot object is also created and /// made the parent of the TargetTransform. /// [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); } } /// /// Destroys all sliders created with CreateSliders and restores the /// 's TargetTransform's parent. /// 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(); 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(); 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(); primitive.GetComponent().material = material; primitive.GetComponent().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 } }