diff --git a/org.mixedrealitytoolkit.input/CHANGELOG.md b/org.mixedrealitytoolkit.input/CHANGELOG.md index bb0000f72..7d559d9e6 100644 --- a/org.mixedrealitytoolkit.input/CHANGELOG.md +++ b/org.mixedrealitytoolkit.input/CHANGELOG.md @@ -2,6 +2,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## Unreleased + +### Changed + +* Updated `InteractionDetector` to work across all `XRRayInteractor` and `NearFarInteractor` implementations, instead of just MRTK-specific `MRTKRayInteractor` implementations. [PR #1090](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1090) + +### Deprecated + +* Deprecated `HasUIHover` and `HasUISelection` from `MRTKRayInteractor` in favor of querying the underlying `TrackedDeviceModel` directly instead. [PR #1090](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1090) + ## [4.0.0-pre.2] - 2025-12-05 ### Changed diff --git a/org.mixedrealitytoolkit.input/InteractionModes/InteractionDetector.cs b/org.mixedrealitytoolkit.input/InteractionModes/InteractionDetector.cs index 268a16102..dbabf6b7f 100644 --- a/org.mixedrealitytoolkit.input/InteractionModes/InteractionDetector.cs +++ b/org.mixedrealitytoolkit.input/InteractionModes/InteractionDetector.cs @@ -6,6 +6,7 @@ using UnityEngine; using UnityEngine.Serialization; using UnityEngine.XR.Interaction.Toolkit.Interactors; +using UnityEngine.XR.Interaction.Toolkit.UI; namespace MixedReality.Toolkit.Input { @@ -85,24 +86,7 @@ public InteractionMode ModeOnSelect } /// - public InteractionMode ModeOnDetection => GetDetectedMode(); - - /// - /// Determines which mode should be set. - /// - /// The detected mode. - private InteractionMode GetDetectedMode() - { - if (interactor.hasSelection) - { - return modeOnSelect; - } - else - { - return modeOnHover; - } - - } + public InteractionMode ModeOnDetection => interactor.hasSelection ? modeOnSelect : modeOnHover; [SerializeField] [FormerlySerializedAs("Controllers")] @@ -122,10 +106,13 @@ public bool IsModeDetected() { bool isDetected = (interactor.hasHover && detectHover) || (interactor.hasSelection && detectSelect); - // Remove if/when XRI sets hasHover/Selection when their ray interactor is hovering/selecting legacy UI. - if (interactor is MRTKRayInteractor rayInteractor) + if (interactor is XRRayInteractor rayInteractor) + { + isDetected |= rayInteractor.TryGetUIModel(out TrackedDeviceModel model) && model.currentRaycast.isValid && (detectHover || (model.select && detectSelect)); + } + else if (interactor is NearFarInteractor nearFarInteractor) { - isDetected |= (rayInteractor.HasUIHover && detectHover) || (rayInteractor.HasUISelection && detectSelect); + isDetected |= nearFarInteractor.TryGetUIModel(out TrackedDeviceModel model) && model.currentRaycast.isValid && (detectHover || (model.select && detectSelect)); } return isDetected; diff --git a/org.mixedrealitytoolkit.input/Interactors/Ray/MRTKRayInteractor.cs b/org.mixedrealitytoolkit.input/Interactors/Ray/MRTKRayInteractor.cs index 821f223a7..d237b1043 100644 --- a/org.mixedrealitytoolkit.input/Interactors/Ray/MRTKRayInteractor.cs +++ b/org.mixedrealitytoolkit.input/Interactors/Ray/MRTKRayInteractor.cs @@ -63,33 +63,14 @@ public GameObject ModeManagedRoot /// /// Is this ray currently hovering a UnityUI/Canvas element? /// + [Obsolete("This property has been deprecated in version 4.0.0. Call " + nameof(TryGetUIModel) + " and use " + nameof(TrackedDeviceModel.currentRaycast.isValid) + " instead.")] public bool HasUIHover => TryGetUIModel(out TrackedDeviceModel model) && model.currentRaycast.isValid; /// /// Is this ray currently selecting a UnityUI/Canvas element? /// - public bool HasUISelection - { - get - { - bool hasUISelection = HasUIHover; -#pragma warning disable CS0618 // isUISelectActive is obsolete - if (forceDeprecatedInput) - { - hasUISelection &= isUISelectActive; - } -#pragma warning restore CS0618 // isUISelectActiver is obsolete - else if (uiPressInput != null) - { - hasUISelection &= uiPressInput.ReadIsPerformed(); - } - else - { - hasUISelection = false; - } - return hasUISelection; - } - } + [Obsolete("This property has been deprecated in version 4.0.0. Call " + nameof(TryGetUIModel) + " and use " + nameof(TrackedDeviceModel.select) + " instead.")] + public bool HasUISelection => TryGetUIModel(out TrackedDeviceModel model) && model.select; /// /// Used to check if the parent controller is tracked or not diff --git a/org.mixedrealitytoolkit.input/Tests/Runtime/InteractionModeManagerTests.cs b/org.mixedrealitytoolkit.input/Tests/Runtime/InteractionModeManagerTests.cs index 86488b7aa..d2bd026af 100644 --- a/org.mixedrealitytoolkit.input/Tests/Runtime/InteractionModeManagerTests.cs +++ b/org.mixedrealitytoolkit.input/Tests/Runtime/InteractionModeManagerTests.cs @@ -68,7 +68,7 @@ public IEnumerator ProximityDetectorTest() } /// - /// Tests the basic Interaction detector. The controller should enter one mode during hover, another during select, and fall back to the default mode during neither + /// Tests the basic Interaction detector. The controller should enter one mode during hover, another during select, and fall back to the default mode during neither. /// [UnityTest] public IEnumerator InteractionDetectorTest() @@ -89,28 +89,40 @@ public IEnumerator InteractionDetectorTest() yield return rightHand.AimAt(cube.transform.position); yield return RuntimeTestUtilities.WaitForUpdates(); - InteractionMode currentMode = rightHandController.GetComponentInChildren().GetComponent().ModeOnHover; - Assert.AreEqual(currentMode, rightHandController.GetComponentInChildren().GetComponent().ModeOnDetection); - ValidateInteractionModeActive(rightHandController, currentMode); + InteractionDetector interactionDetector = rightHandController.GetComponentInChildren().GetComponent(); + + InteractionMode expectedMode = interactionDetector.ModeOnHover; + Assert.AreEqual(expectedMode, interactionDetector.ModeOnDetection); + ValidateInteractionModeActive(rightHandController, expectedMode); + // Select the cube and check that we're in the correct mode yield return rightHand.SetHandshape(HandshapeTypes.HandshapeId.Grab); yield return RuntimeTestUtilities.WaitForUpdates(); - currentMode = rightHandController.GetComponentInChildren().GetComponent().ModeOnSelect; - Assert.AreEqual(currentMode, rightHandController.GetComponentInChildren().GetComponent().ModeOnDetection); - ValidateInteractionModeActive(rightHandController, currentMode); + expectedMode = interactionDetector.ModeOnSelect; + Assert.AreEqual(expectedMode, interactionDetector.ModeOnDetection); + ValidateInteractionModeActive(rightHandController, expectedMode); - // move the hand far away and validate that we are in the default mode + // Release the selection and move the hand far away and validate that we are in the default mode yield return rightHand.SetHandshape(HandshapeTypes.HandshapeId.Open); yield return RuntimeTestUtilities.WaitForUpdates(); - yield return rightHand.MoveTo(cube.transform.position + new Vector3(3.0f,0,0)); + yield return rightHand.MoveTo(cube.transform.position + new Vector3(3.0f, 0, 0)); yield return RuntimeTestUtilities.WaitForUpdates(); + expectedMode = InteractionModeManager.Instance.DefaultMode; + ValidateInteractionModeActive(rightHandController, expectedMode); - currentMode = InteractionModeManager.Instance.DefaultMode; - ValidateInteractionModeActive(rightHandController, currentMode); + // Put the hand into a grab state and validate that we are in the default mode, since we're not selecting an object + yield return rightHand.SetHandshape(HandshapeTypes.HandshapeId.Grab); + yield return RuntimeTestUtilities.WaitForUpdates(); + ValidateInteractionModeActive(rightHandController, expectedMode); + + // Release the grab state and validate that we are in the default mode + yield return rightHand.SetHandshape(HandshapeTypes.HandshapeId.Open); + yield return RuntimeTestUtilities.WaitForUpdates(); + ValidateInteractionModeActive(rightHandController, expectedMode); } /// - /// Tests that mode mediation works properly. + /// Tests that mode mediation works properly. /// /// /// The interaction mode with the higher priority should be the valid one which affects the controller. @@ -136,12 +148,14 @@ public IEnumerator ModeMediationTest() InputTestUtilities.SetHandAnchorPoint(Handedness.Right, ControllerAnchorPoint.Grab); yield return RuntimeTestUtilities.WaitForUpdates(); - // Moving the hand to a position where it's far ray is hovering over the cube + InteractionDetector rayInteractionDetector = rightHandController.GetComponentInChildren().GetComponent(); + + // Moving the hand to a position where its far ray is hovering over the cube yield return rightHand.AimAt(cube.transform.position); yield return RuntimeTestUtilities.WaitForUpdates(); - InteractionMode farRayMode = rightHandController.GetComponentInChildren().GetComponent().ModeOnHover; + InteractionMode farRayMode = rayInteractionDetector.ModeOnHover; yield return RuntimeTestUtilities.WaitForUpdates(); - Assert.AreEqual(farRayMode, rightHandController.GetComponentInChildren().GetComponent().ModeOnDetection); + Assert.AreEqual(farRayMode, rayInteractionDetector.ModeOnDetection); ValidateInteractionModeActive(rightHandController, farRayMode); // Now move the hand in range for the proximity detector @@ -159,8 +173,10 @@ public IEnumerator ModeMediationTest() yield return rightHand.SetHandshape(HandshapeTypes.HandshapeId.Grab); yield return RuntimeTestUtilities.WaitForUpdates(); - InteractionMode grabMode = rightHandController.GetComponentInChildren().GetComponent().ModeOnSelect; - Assert.AreEqual(grabMode, rightHandController.GetComponentInChildren().GetComponent().ModeOnDetection); + InteractionDetector grabInteractionDetector = rightHandController.GetComponentInChildren().GetComponent(); + + InteractionMode grabMode = grabInteractionDetector.ModeOnSelect; + Assert.AreEqual(grabMode, grabInteractionDetector.ModeOnDetection); yield return RuntimeTestUtilities.WaitForUpdates(); ValidateInteractionModeActive(rightHandController, grabMode); Assert.IsTrue(grabMode.Priority > nearMode.Priority); @@ -176,7 +192,7 @@ public IEnumerator ModeMediationTest() // Moving the hand to a position where it's far ray is hovering over the cube yield return rightHand.MoveTo(cube.transform.position + new Vector3(0.02f, -0.1f, -0.8f)); - yield return RuntimeTestUtilities.WaitForUpdates(frameCount:120); + yield return RuntimeTestUtilities.WaitForUpdates(frameCount: 120); ValidateInteractionModeActive(rightHandController, farRayMode); } @@ -189,15 +205,14 @@ public IEnumerator ModeMediationTest() private void ValidateInteractionModeActive(XRBaseController controller, InteractionMode currentMode) { // We construct the list of managed interactor types manually because we don't want to expose the internal controller mapping implementation to even internal use, since - // we don't want any other class to be able to modify those collections without going through the Mode Manager or it's in-editor inspector. - HashSet managedInteractorTypes = new HashSet(InteractionModeManager.Instance.PrioritizedInteractionModes.SelectMany(x => x.AssociatedTypes)); - HashSet activeInteractorTypes = InteractionModeManager.Instance.PrioritizedInteractionModes.Find(x => x.ModeName == currentMode.Name).AssociatedTypes; + // we don't want any other class to be able to modify those collections without going through the Mode Manager or its in-editor inspector. + HashSet managedInteractorTypes = new HashSet(InteractionModeManager.Instance.PrioritizedInteractionModes.SelectMany(x => x.AssociatedTypes)); + HashSet activeInteractorTypes = InteractionModeManager.Instance.PrioritizedInteractionModes.Find(x => x.ModeName == currentMode.Name).AssociatedTypes; // Ensure the prox detector has actually had the desired effect of enabling/disabling interactors. - foreach (System.Type interactorType in managedInteractorTypes) + foreach (Type interactorType in managedInteractorTypes) { - XRBaseInteractor interactor = controller.GetComponentInChildren(interactorType) as XRBaseInputInteractor; - if (interactor != null) + if (controller.GetComponentInChildren(interactorType) is XRBaseInputInteractor interactor && interactor != null) { Assert.AreEqual(activeInteractorTypes.Contains(interactorType), interactor.enabled); } diff --git a/org.mixedrealitytoolkit.input/Tests/Runtime/InteractionModeManagerTestsForControllerlessRig.cs b/org.mixedrealitytoolkit.input/Tests/Runtime/InteractionModeManagerTestsForControllerlessRig.cs index bee2d50be..164917c09 100644 --- a/org.mixedrealitytoolkit.input/Tests/Runtime/InteractionModeManagerTestsForControllerlessRig.cs +++ b/org.mixedrealitytoolkit.input/Tests/Runtime/InteractionModeManagerTestsForControllerlessRig.cs @@ -91,30 +91,42 @@ public IEnumerator InteractionDetectorTest() yield return RuntimeTestUtilities.WaitForUpdates(); TrackedPoseDriver rightHandTrackedPoseDriver = CachedTrackedPoseDriverLookup.RightHandTrackedPoseDriver; - InteractionDetector rightHandInteractionDetector = rightHandTrackedPoseDriver.transform.parent.GetComponentInChildren().GetComponent(); + Assert.IsTrue(rightHandTrackedPoseDriver != null, "No tracked pose driver found for right hand."); // Moving the hand to a position where it's far ray is hovering over the cube yield return rightHand.AimAt(cube.transform.position); yield return RuntimeTestUtilities.WaitForUpdates(); - InteractionMode currentMode = rightHandInteractionDetector.ModeOnHover; - Assert.AreEqual(currentMode, rightHandInteractionDetector.ModeOnDetection); - ValidateInteractionModeActive(rightHandTrackedPoseDriver, currentMode); + InteractionDetector interactionDetector = rightHandTrackedPoseDriver.transform.parent.GetComponentInChildren().GetComponent(); + + InteractionMode expectedMode = interactionDetector.ModeOnHover; + Assert.AreEqual(expectedMode, interactionDetector.ModeOnDetection); + ValidateInteractionModeActive(rightHandTrackedPoseDriver, expectedMode); + // Select the cube and check that we're in the correct mode yield return rightHand.SetHandshape(HandshapeTypes.HandshapeId.Grab); yield return RuntimeTestUtilities.WaitForUpdates(); - currentMode = rightHandInteractionDetector.ModeOnSelect; - Assert.AreEqual(currentMode, rightHandInteractionDetector.ModeOnDetection); - ValidateInteractionModeActive(rightHandTrackedPoseDriver, currentMode); + expectedMode = interactionDetector.ModeOnSelect; + Assert.AreEqual(expectedMode, interactionDetector.ModeOnDetection); + ValidateInteractionModeActive(rightHandTrackedPoseDriver, expectedMode); - // move the hand far away and validate that we are in the default mode + // Release the selection and move the hand far away and validate that we are in the default mode yield return rightHand.SetHandshape(HandshapeTypes.HandshapeId.Open); yield return RuntimeTestUtilities.WaitForUpdates(); - yield return rightHand.MoveTo(cube.transform.position + new Vector3(3.0f,0,0)); + yield return rightHand.MoveTo(cube.transform.position + new Vector3(3.0f, 0, 0)); yield return RuntimeTestUtilities.WaitForUpdates(); + expectedMode = InteractionModeManager.Instance.DefaultMode; + ValidateInteractionModeActive(rightHandTrackedPoseDriver, expectedMode); - currentMode = InteractionModeManager.Instance.DefaultMode; - ValidateInteractionModeActive(rightHandTrackedPoseDriver, currentMode); + // Put the hand into a grab state and validate that we are in the default mode, since we're not selecting an object + yield return rightHand.SetHandshape(HandshapeTypes.HandshapeId.Grab); + yield return RuntimeTestUtilities.WaitForUpdates(); + ValidateInteractionModeActive(rightHandTrackedPoseDriver, expectedMode); + + // Release the grab state and validate that we are in the default mode + yield return rightHand.SetHandshape(HandshapeTypes.HandshapeId.Open); + yield return RuntimeTestUtilities.WaitForUpdates(); + ValidateInteractionModeActive(rightHandTrackedPoseDriver, expectedMode); } ///