diff --git a/org.mixedrealitytoolkit.uxcomponents.noncanvas/Tests/Runtime/SliderTests.cs b/org.mixedrealitytoolkit.uxcomponents.noncanvas/Tests/Runtime/SliderTests.cs index abce0d0e6..34d1fd670 100644 --- a/org.mixedrealitytoolkit.uxcomponents.noncanvas/Tests/Runtime/SliderTests.cs +++ b/org.mixedrealitytoolkit.uxcomponents.noncanvas/Tests/Runtime/SliderTests.cs @@ -19,9 +19,9 @@ namespace MixedReality.Toolkit.UX.Runtime.Tests public class SliderTests : BaseRuntimeInputTests { // UXComponents/Sliders/Prefabs/NonCanvasSliderBase.prefab - private const string defaultSliderPrefabGuid = "5cf5d5d5cc7fe184a93c388dbab66bb9"; - private static readonly string defaultSliderPrefabPath = AssetDatabase.GUIDToAssetPath(defaultSliderPrefabGuid); - private const float sliderValueDelta = 0.02f; + private const string DefaultSliderPrefabGuid = "5cf5d5d5cc7fe184a93c388dbab66bb9"; + private static readonly string DefaultSliderPrefabPath = AssetDatabase.GUIDToAssetPath(DefaultSliderPrefabGuid); + private const float SliderValueDelta = 0.02f; public override IEnumerator Setup() { @@ -57,10 +57,10 @@ public IEnumerator TestAssembleInteractableAndNearManip() // This should not throw exception AssembleSlider(InputTestUtilities.InFrontOfUser(Vector3.forward), Vector3.zero, out GameObject sliderObject, out Slider slider, out _); - Assert.AreEqual(0.5f, slider.Value, sliderValueDelta, "Slider should have value 0.5 at start"); + Assert.AreEqual(0.5f, slider.Value, SliderValueDelta, "Slider should have value 0.5 at start"); yield return DirectPinchAndMoveSlider(slider, 1.0f); // Allow some leeway due to grab positions shifting on open - Assert.AreEqual(1.0f, slider.Value, sliderValueDelta, "Slider should have value 1.0 after being manipulated at start"); + Assert.AreEqual(1.0f, slider.Value, SliderValueDelta, "Slider should have value 1.0 after being manipulated at start"); //// clean up Object.Destroy(sliderObject); @@ -75,10 +75,10 @@ public IEnumerator TestLoadPrefabAndNearManipNoSnap() slider.SnapToPosition = false; slider.IsTouchable = false; - Assert.AreEqual(0.5f, slider.Value, sliderValueDelta, "Slider should have value 0.5 at start"); + Assert.AreEqual(0.5f, slider.Value, SliderValueDelta, "Slider should have value 0.5 at start"); yield return DirectPinchAndMoveSlider(slider, 1.0f); // Allow some leeway due to grab positions shifting on open - Assert.AreEqual(1.0f, slider.Value, sliderValueDelta, "Slider should have value 1.0 after being manipulated at start"); + Assert.AreEqual(1.0f, slider.Value, SliderValueDelta, "Slider should have value 1.0 after being manipulated at start"); // clean up Object.Destroy(sliderObject); @@ -435,7 +435,7 @@ public IEnumerator TestNullVisualsNoSnap() // Test that the slider still works and no errors are thrown yield return RuntimeTestUtilities.WaitForUpdates(); - Assert.AreEqual(0.5f, slider.Value, sliderValueDelta, "Slider should have value 0.5 at start"); + Assert.AreEqual(0.5f, slider.Value, SliderValueDelta, "Slider should have value 0.5 at start"); // clean up Object.Destroy(sliderObject); @@ -669,7 +669,7 @@ private void AssembleSlider(Vector3 position, Vector3 rotation, out GameObject s private void InstantiateDefaultSliderPrefab(Vector3 position, Vector3 rotation, out GameObject sliderObject, out Slider slider, out SliderVisuals sliderVisuals) { // Load interactable prefab - Object sliderPrefab = AssetDatabase.LoadAssetAtPath(defaultSliderPrefabPath, typeof(Object)); + Object sliderPrefab = AssetDatabase.LoadAssetAtPath(DefaultSliderPrefabPath, typeof(Object)); sliderObject = Object.Instantiate(sliderPrefab) as GameObject; slider = sliderObject.GetComponent(); sliderVisuals = sliderObject.GetComponent(); diff --git a/org.mixedrealitytoolkit.uxcomponents/Tests/Runtime/SliderTests.cs b/org.mixedrealitytoolkit.uxcomponents/Tests/Runtime/SliderTests.cs new file mode 100644 index 000000000..375f192c4 --- /dev/null +++ b/org.mixedrealitytoolkit.uxcomponents/Tests/Runtime/SliderTests.cs @@ -0,0 +1,194 @@ +// Copyright (c) Mixed Reality Toolkit Contributors +// Licensed under the BSD 3-Clause + +// Disable "missing XML comment" warning for tests. While nice to have, this documentation is not required. +#pragma warning disable CS1591 + +using MixedReality.Toolkit.Core.Tests; +using MixedReality.Toolkit.Input; +using MixedReality.Toolkit.Input.Tests; +using NUnit.Framework; +using System.Collections; +using UnityEditor; +using UnityEngine; +using UnityEngine.TestTools; +using UnityEngine.UI; +using Object = UnityEngine.Object; + +namespace MixedReality.Toolkit.UX.Runtime.Tests +{ + public class SliderTests : BaseRuntimeInputTests + { + // Slider/CanvasSlider.prefab + private const string DefaultSliderPrefabGuid = "f64620d502cdf0f429efa27703913cb7"; + private static readonly string DefaultSliderPrefabPath = AssetDatabase.GUIDToAssetPath(DefaultSliderPrefabGuid); + + private const int HandMovementFrames = 10; + + private TestHand hand; + + [SetUp] + public void SetUp() + { + hand = new TestHand(Handedness.Right); + } + + [UnityTest] + public IEnumerator TouchSlider_MoveRight_ValueIncreasesCorrectly([ValueSource(nameof(MoveRightTestCases))] TestCase testCase) + { + InputTestUtilities.InitializeCameraToOriginAndForward(); + + var testPrefab = InstantiateSlider(DefaultSliderPrefabPath); + testPrefab.transform.position = InputTestUtilities.InFrontOfUser(new Vector3(0, 0, 1)); + + var slider = testPrefab.GetComponentInChildren(); + slider.MinValue = testCase.MinValue; + slider.MaxValue = testCase.MaxValue; + slider.Value = ((testCase.MaxValue - testCase.MinValue) / 2) + testCase.MinValue; + + yield return ShowHand(); + yield return hand.MoveTo(testPrefab.transform.position - new Vector3(0, 0, 0.00f), HandMovementFrames); + yield return RuntimeTestUtilities.WaitForUpdates(); + yield return hand.MoveTo(testPrefab.transform.position - new Vector3(-0.04f, 0f, 0.00f), HandMovementFrames); + yield return RuntimeTestUtilities.WaitForUpdates(); + Assert.That(slider.Value, Is.EqualTo(testCase.Expected).Within(0.00001f)); + + Object.Destroy(testPrefab); + } + + [UnityTest] + public IEnumerator GrabSlider_MoveRight_ValueIncreasesCorrectly([ValueSource(nameof(MoveRightTestCases))] TestCase testCase) + { + InputTestUtilities.InitializeCameraToOriginAndForward(); + + var testPrefab = InstantiateSlider(DefaultSliderPrefabPath); + testPrefab.transform.position = InputTestUtilities.InFrontOfUser(new Vector3(0, 0, 1)); + + var slider = testPrefab.GetComponentInChildren(); + slider.IsTouchable = false; + slider.MinValue = testCase.MinValue; + slider.MaxValue = testCase.MaxValue; + slider.Value = ((testCase.MaxValue - testCase.MinValue) / 2) + testCase.MinValue; + + yield return ShowHand(); + yield return hand.MoveTo(testPrefab.transform.position - new Vector3(0, 0, 0.00f), HandMovementFrames); + yield return RuntimeTestUtilities.WaitForUpdates(); + yield return hand.SetHandshape(HandshapeTypes.HandshapeId.Grab); + yield return RuntimeTestUtilities.WaitForUpdates(); + yield return hand.MoveTo(testPrefab.transform.position - new Vector3(-0.04f, 0f, 0.00f), HandMovementFrames); + yield return RuntimeTestUtilities.WaitForUpdates(); + yield return hand.SetHandshape(HandshapeTypes.HandshapeId.Open); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.That(slider.Value, Is.EqualTo(testCase.Expected).Within(0.00001f)); + + Object.Destroy(testPrefab); + } + [UnityTest] + public IEnumerator TouchSlider_MoveLeft_ValueIncreasesCorrectly([ValueSource(nameof(MoveLeftTestCases))] TestCase testCase) + { + InputTestUtilities.InitializeCameraToOriginAndForward(); + + var testPrefab = InstantiateSlider(DefaultSliderPrefabPath); + testPrefab.transform.position = InputTestUtilities.InFrontOfUser(new Vector3(0, 0, 1)); + + var slider = testPrefab.GetComponentInChildren(); + slider.MinValue = testCase.MinValue; + slider.MaxValue = testCase.MaxValue; + slider.Value = ((testCase.MaxValue - testCase.MinValue) / 2) + testCase.MinValue; + + yield return ShowHand(); + yield return hand.MoveTo(testPrefab.transform.position - new Vector3(0, 0, 0.00f), HandMovementFrames); + yield return RuntimeTestUtilities.WaitForUpdates(); + yield return hand.MoveTo(testPrefab.transform.position + new Vector3(-0.04f, 0f, 0.00f), HandMovementFrames); + yield return RuntimeTestUtilities.WaitForUpdates(); + Assert.That(slider.Value, Is.EqualTo(testCase.Expected).Within(0.00001f)); + + Object.Destroy(testPrefab); + } + + [UnityTest] + public IEnumerator GrabSlider_MoveLeft_ValueIncreasesCorrectly([ValueSource(nameof(MoveLeftTestCases))] TestCase testCase) + { + InputTestUtilities.InitializeCameraToOriginAndForward(); + + var testPrefab = InstantiateSlider(DefaultSliderPrefabPath); + testPrefab.transform.position = InputTestUtilities.InFrontOfUser(new Vector3(0, 0, 1)); + + var slider = testPrefab.GetComponentInChildren(); + slider.IsTouchable = false; + slider.MinValue = testCase.MinValue; + slider.MaxValue = testCase.MaxValue; + slider.Value = ((testCase.MaxValue - testCase.MinValue) / 2) + testCase.MinValue; + + yield return ShowHand(); + yield return hand.MoveTo(testPrefab.transform.position - new Vector3(0, 0, 0.00f), HandMovementFrames); + yield return RuntimeTestUtilities.WaitForUpdates(); + yield return hand.SetHandshape(HandshapeTypes.HandshapeId.Grab); + yield return RuntimeTestUtilities.WaitForUpdates(); + yield return hand.MoveTo(testPrefab.transform.position + new Vector3(-0.04f, 0f, 0.00f), HandMovementFrames); + yield return RuntimeTestUtilities.WaitForUpdates(); + yield return hand.SetHandshape(HandshapeTypes.HandshapeId.Open); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.That(slider.Value, Is.EqualTo(testCase.Expected).Within(0.00001f)); + + Object.Destroy(testPrefab); + } + + public readonly struct TestCase + { + public float MinValue { get; } + public float MaxValue { get; } + public float Expected { get; } + + public TestCase(float minValue, float maxValue, float expected) + { + MinValue = minValue; + MaxValue = maxValue; + Expected = expected; + } + } + + private static IEnumerable MoveRightTestCases() + { + yield return new TestCase(minValue: 0, maxValue: 1, expected: 0.7f); + yield return new TestCase(minValue: 0, maxValue: 10, expected: 7); + yield return new TestCase(minValue: 0, maxValue: 0.1f, expected: 0.07f); + yield return new TestCase(minValue: -1, maxValue: 1, expected: 0.4f); + yield return new TestCase(minValue: -1, maxValue: 0, expected: -0.3f); + } + + private static IEnumerable MoveLeftTestCases() + { + yield return new TestCase(minValue: 0, maxValue: 1, expected: 0.3f); + yield return new TestCase(minValue: 0, maxValue: 10, expected: 3); + yield return new TestCase(minValue: 0, maxValue: 0.1f, expected: 0.03f); + yield return new TestCase(minValue: -1, maxValue: 1, expected: -0.4f); + yield return new TestCase(minValue: -1, maxValue: 0, expected: -0.7f); + } + + private IEnumerator ShowHand() + { + Vector3 initialHandPosition = InputTestUtilities.InFrontOfUser(new Vector3(0.05f, -0.05f, 0.3f)); + yield return hand.Show(initialHandPosition); + yield return RuntimeTestUtilities.WaitForUpdates(); + } + + private GameObject InstantiateSlider(string prefabPath) + { + GameObject canvas = new GameObject("SliderParent", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler)); + canvas.transform.localScale = Vector3.one * 0.001f; + (canvas.transform as RectTransform).sizeDelta = Vector2.one * 200; + GameObject slider = Object.Instantiate(AssetDatabase.LoadAssetAtPath(prefabPath)); + slider.transform.SetParent(canvas.transform, worldPositionStays: false); + slider.transform.position = Vector3.zero; + if (slider.transform is RectTransform rectTransform) + { + rectTransform.sizeDelta = new Vector2(200f, rectTransform.sizeDelta.y); + } + return canvas; + } + } +} +#pragma warning restore CS1591 diff --git a/org.mixedrealitytoolkit.uxcomponents/Tests/Runtime/SliderTests.cs.meta b/org.mixedrealitytoolkit.uxcomponents/Tests/Runtime/SliderTests.cs.meta new file mode 100644 index 000000000..30c04f669 --- /dev/null +++ b/org.mixedrealitytoolkit.uxcomponents/Tests/Runtime/SliderTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 32287a005eb5f72449ea29262d9d660f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/org.mixedrealitytoolkit.uxcore/CHANGELOG.md b/org.mixedrealitytoolkit.uxcore/CHANGELOG.md index 9dff22b8e..98fb03603 100644 --- a/org.mixedrealitytoolkit.uxcore/CHANGELOG.md +++ b/org.mixedrealitytoolkit.uxcore/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). * Prevent simultaneous editing of multiple input fields when using Non-Native Keyboard. [PR #942](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/942) * Don't try to start a coroutine in VirtualizedScrollRectList when the GameObject is inactive. [PR #972](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/972) * Fix SliderSounds playing sound even when disabled. [PR #1007](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1007) +* Fix for sliders with min-max values outside range \[0-1\] when slider is configured for grab interaction (IsTouchable = false) [Issue 944](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/issues/944) ## [3.2.2] - 2024-09-18 diff --git a/org.mixedrealitytoolkit.uxcore/Slider/Slider.cs b/org.mixedrealitytoolkit.uxcore/Slider/Slider.cs index e75e64296..c0bc6b755 100644 --- a/org.mixedrealitytoolkit.uxcore/Slider/Slider.cs +++ b/org.mixedrealitytoolkit.uxcore/Slider/Slider.cs @@ -370,7 +370,8 @@ private void UpdateSliderValue() var handDelta = Vector3.Dot(SliderTrackDirection.normalized, interactorDelta); - float normalizedValue = Mathf.Clamp(StartSliderValue + handDelta / SliderTrackDirection.magnitude, 0f, 1.0f); + var normalizedStartValue = (StartSliderValue - MinValue) / (MaxValue - MinValue); + float normalizedValue = Mathf.Clamp(normalizedStartValue + handDelta / SliderTrackDirection.magnitude, 0f, 1.0f); var unsnappedValue = normalizedValue * (MaxValue - MinValue) + MinValue; Value = useSliderStepDivisions ? SnapSliderToStepPositions(unsnappedValue) : unsnappedValue; @@ -386,12 +387,12 @@ protected override void OnSelectEntered(SelectEnterEventArgs args) base.OnSelectEntered(args); // Snap to position by setting the startPosition - // to the slider start, and start value to zero. + // to the slider start, and start value to MinValue. // However, don't snap when using grabs. if (snapToPosition && !(args.interactorObject is IGrabInteractor)) { StartInteractionPoint = SliderStart.position; - StartSliderValue = 0.0f; + StartSliderValue = MinValue; } else {