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);
}
///