diff --git a/org.mixedrealitytoolkit.spatialmanipulation/CHANGELOG.md b/org.mixedrealitytoolkit.spatialmanipulation/CHANGELOG.md index 8d296cfd2..9c58aecca 100644 --- a/org.mixedrealitytoolkit.spatialmanipulation/CHANGELOG.md +++ b/org.mixedrealitytoolkit.spatialmanipulation/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). * Fix null ref in SpatialManipulationReticle when multiple interactables are hovered. [PR #873](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/873) * ConstantViewSize solver now retains the initial scale and aspect ratio [PR #719](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/719) * Fixed Follow solver frequently logging "Look Rotation Viewing Vector Is Zero" [PR #895](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/895) +* Fixed issues with HandConstraint hand tracking events not being fired (OnFirstHandDetected/OnHandActivate/OnLastHandLost/OnHandDeactivate) when SolverHandler TrackedHand is set to Right or Left (not Both) [PR #956](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/956) ## [3.3.0] - 2024-04-30 diff --git a/org.mixedrealitytoolkit.spatialmanipulation/Solvers/HandConstraint.cs b/org.mixedrealitytoolkit.spatialmanipulation/Solvers/HandConstraint.cs index 30a83c7a2..bbc933ccf 100644 --- a/org.mixedrealitytoolkit.spatialmanipulation/Solvers/HandConstraint.cs +++ b/org.mixedrealitytoolkit.spatialmanipulation/Solvers/HandConstraint.cs @@ -222,8 +222,6 @@ public override void SolverUpdate() return; } - XRNode? prevTrackedNode = trackedNode; - if (SolverHandler.CurrentTrackedHandedness != Handedness.None) { trackedNode = GetControllerNode(SolverHandler.CurrentTrackedHandedness); @@ -678,12 +676,11 @@ private bool IsPalmFacingCamera(XRNode? hand) /// /// Returns the XRNode that matches the desired handedness. /// - /// The handedness of the returned controller - /// The IMixedRealityController for the desired handedness, or null if none are present. + /// The handedness of the returned controller node. + /// The for the desired handedness, or null if none are present. protected XRNode? GetControllerNode(Handedness handedness) { - if (!SolverHandler.IsValidHandedness(handedness)) { return null; } - return (handedness == Handedness.Left) ? XRNode.LeftHand : XRNode.RightHand; + return !SolverHandler.IsValidHandedness(handedness) ? null : handedness.ToXRNode(); } #region MonoBehaviour Implementation diff --git a/org.mixedrealitytoolkit.spatialmanipulation/Solvers/SolverHandler.cs b/org.mixedrealitytoolkit.spatialmanipulation/Solvers/SolverHandler.cs index 8e649442e..93696bbd0 100644 --- a/org.mixedrealitytoolkit.spatialmanipulation/Solvers/SolverHandler.cs +++ b/org.mixedrealitytoolkit.spatialmanipulation/Solvers/SolverHandler.cs @@ -1,7 +1,6 @@ // Copyright (c) Mixed Reality Toolkit Contributors // Licensed under the BSD 3-Clause -using MixedReality.Toolkit.Subsystems; using System.Collections.Generic; using System.Linq; using Unity.Profiling; @@ -251,16 +250,13 @@ public Transform TransformTarget } } - // Stores currently attached hand if valid (only possible values Left, Right, or None) - private Handedness currentTrackedHandedness = Handedness.None; - /// /// Currently tracked hand or motion controller, if applicable. /// /// - /// The allowable values are Left, Right, or None + /// The allowable values are Left, Right, or None. /// - public Handedness CurrentTrackedHandedness => currentTrackedHandedness; + public Handedness CurrentTrackedHandedness { get; private set; } = Handedness.None; // Stores controller side to favor if TrackedHandedness is set to both left and right. private Handedness preferredTrackedHandedness = Handedness.Left; @@ -424,7 +420,7 @@ internal void UnregisterSolver(Solver solver) } /// - /// Clear the parent of the internally created tracking-target game object. + /// Clear the parent of the internally created tracking-target game object. /// /// /// A tracking-target is created when is called, and @@ -452,7 +448,7 @@ protected virtual void AttachToNewTrackedObject() { using (AttachToNewTrackedObjectPerfMarker.Auto()) { - currentTrackedHandedness = Handedness.None; + CurrentTrackedHandedness = Handedness.None; controllerInteractor = null; Transform target = null; @@ -462,54 +458,52 @@ protected virtual void AttachToNewTrackedObject() } else if (TrackedTargetType == TrackedObjectType.ControllerRay) { - currentTrackedHandedness = TrackedHandedness; - if ((currentTrackedHandedness & Handedness.Both) == Handedness.Both) + CurrentTrackedHandedness = TrackedHandedness; + if ((CurrentTrackedHandedness & Handedness.Both) == Handedness.Both) { - currentTrackedHandedness = PreferredTrackedHandedness; - controllerInteractor = GetControllerInteractor(currentTrackedHandedness); + CurrentTrackedHandedness = PreferredTrackedHandedness; + controllerInteractor = GetControllerInteractor(CurrentTrackedHandedness); if (controllerInteractor == null || !controllerInteractor.isHoverActive) { // If no interactor found, try again on the opposite hand - currentTrackedHandedness = currentTrackedHandedness.GetOppositeHandedness(); - controllerInteractor = GetControllerInteractor(currentTrackedHandedness); + CurrentTrackedHandedness = PreferredTrackedHandedness.GetOppositeHandedness(); + controllerInteractor = GetControllerInteractor(CurrentTrackedHandedness); } } else { - controllerInteractor = GetControllerInteractor(currentTrackedHandedness); + controllerInteractor = GetControllerInteractor(CurrentTrackedHandedness); } - if (controllerInteractor != null) + if (controllerInteractor != null && controllerInteractor.isHoverActive) { target = controllerInteractor.transform; } else { - currentTrackedHandedness = Handedness.None; + CurrentTrackedHandedness = Handedness.None; } } else if (TrackedTargetType == TrackedObjectType.HandJoint) { if (XRSubsystemHelpers.HandsAggregator != null) { - currentTrackedHandedness = TrackedHandedness; - if ((currentTrackedHandedness & Handedness.Both) == Handedness.Both) + CurrentTrackedHandedness = TrackedHandedness; + if ((CurrentTrackedHandedness & Handedness.Both) == Handedness.Both) { - if (IsHandTracked(PreferredTrackedHandedness)) - { - currentTrackedHandedness = PreferredTrackedHandedness; - } - else if (IsHandTracked(PreferredTrackedHandedness.GetOppositeHandedness())) - { - currentTrackedHandedness = PreferredTrackedHandedness.GetOppositeHandedness(); - } - else + CurrentTrackedHandedness = PreferredTrackedHandedness; + if (!IsHandTracked(CurrentTrackedHandedness)) { - currentTrackedHandedness = Handedness.None; + CurrentTrackedHandedness = PreferredTrackedHandedness.GetOppositeHandedness(); } } + if (!IsHandTracked(CurrentTrackedHandedness)) + { + CurrentTrackedHandedness = Handedness.None; + } + if (UpdateCachedHandJointTransform()) { target = cachedHandJointTransform; @@ -518,7 +512,7 @@ protected virtual void AttachToNewTrackedObject() } else if (TrackedTargetType == TrackedObjectType.CustomOverride) { - target = this.transformOverride; + target = transformOverride; } TrackTransform(target); @@ -529,7 +523,7 @@ protected virtual void AttachToNewTrackedObject() new ProfilerMarker("[MRTK] SolverHandler.UpdateCachedHandJointTransform"); /// - /// Update the cached transform's position to match that of the current track joint. + /// Update the cached transform's position to match that of the current track joint. /// /// /// if the tracked joint is found and cached transform is updated, otherwise. @@ -539,7 +533,7 @@ private bool UpdateCachedHandJointTransform() bool updated = false; using (UpdateCachedHandJointTransformPerfMarker.Auto()) { - XRNode? handNode = currentTrackedHandedness.ToXRNode(); + XRNode? handNode = CurrentTrackedHandedness.ToXRNode(); if (handNode.HasValue && XRSubsystemHelpers.HandsAggregator != null && @@ -571,7 +565,6 @@ private void TrackTransform(Transform target) trackingTarget.transform.localPosition = Vector3.Scale(AdditionalOffset, trackingTarget.transform.localScale); trackingTarget.transform.localRotation = Quaternion.Euler(AdditionalRotation); } - } /// @@ -613,10 +606,10 @@ private bool IsInvalidTracking() } // If we were tracking a particular hand, check that our transform is still valid - if (TrackedTargetType == TrackedObjectType.HandJoint && currentTrackedHandedness != Handedness.None) + if (TrackedTargetType == TrackedObjectType.HandJoint && CurrentTrackedHandedness != Handedness.None) { - bool trackingLeft = currentTrackedHandedness.IsMatch(Handedness.Left) && IsHandTracked(Handedness.Left); - bool trackingRight = currentTrackedHandedness.IsMatch(Handedness.Right) && IsHandTracked(Handedness.Right); + bool trackingLeft = CurrentTrackedHandedness.IsMatch(Handedness.Left) && IsHandTracked(Handedness.Left); + bool trackingRight = CurrentTrackedHandedness.IsMatch(Handedness.Right) && IsHandTracked(Handedness.Right); return !trackingLeft && !trackingRight; } @@ -630,14 +623,14 @@ private bool IsInvalidTracking() /// /// if the hand is tracked, or . /// - private bool IsHandTracked(Handedness hand) + private static bool IsHandTracked(Handedness hand) { // Early out if the hand isn't a valid XRNode // (i.e., Handedness.None or Handedness.Both) XRNode? node = hand.ToXRNode(); - if (!node.HasValue) { return false; } - return XRSubsystemHelpers.HandsAggregator != null && - XRSubsystemHelpers.HandsAggregator.TryGetJoint(TrackedHandJoint.Palm, node.Value, out HandJointPose pose); + return node.HasValue && + XRSubsystemHelpers.HandsAggregator != null && + XRSubsystemHelpers.HandsAggregator.TryGetJoint(TrackedHandJoint.Palm, node.Value, out _); } /// diff --git a/org.mixedrealitytoolkit.spatialmanipulation/Tests/Runtime/HandConstraintTests.cs b/org.mixedrealitytoolkit.spatialmanipulation/Tests/Runtime/HandConstraintTests.cs new file mode 100644 index 000000000..cfa274fe7 --- /dev/null +++ b/org.mixedrealitytoolkit.spatialmanipulation/Tests/Runtime/HandConstraintTests.cs @@ -0,0 +1,165 @@ +// 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.Tests; +using NUnit.Framework; +using System.Collections; +using UnityEngine; +using UnityEngine.TestTools; + +namespace MixedReality.Toolkit.SpatialManipulation.Runtime.Tests +{ + /// + /// Tests for . + /// + public class HandConstraintTests : BaseRuntimeInputTests + { + /// + /// This checks if the HandConstraint events properly fire when the tracked handedness is set to a single hand. + /// + [UnityTest] + public IEnumerator HandConstraintEventsOneHanded() + { + // Disable gaze interactions for this unit test; + InputTestUtilities.DisableGazeInteractor(); + + // Set up GameObject with a SolverHandler + GameObject testObject = GameObject.CreatePrimitive(PrimitiveType.Cube); + SolverHandler solverHandler = testObject.AddComponent(); + + // Set it to track interactors + solverHandler.TrackedHandedness = Handedness.Right; + solverHandler.TrackedTargetType = TrackedObjectType.HandJoint; + solverHandler.TrackedHandJoint = TrackedHandJoint.Palm; + + // Set up GameObject with a HandConstraint + HandConstraint handConstraint = testObject.AddComponent(); + + bool onFirstHandDetected = false; + bool onHandActivate = false; + bool onHandDeactivate = false; + bool onLastHandLost = false; + + handConstraint.OnFirstHandDetected.AddListener(() => onFirstHandDetected = true); + handConstraint.OnHandActivate.AddListener(() => onHandActivate = true); + handConstraint.OnHandDeactivate.AddListener(() => onHandDeactivate = true); + handConstraint.OnLastHandLost.AddListener(() => onLastHandLost = true); + + yield return RuntimeTestUtilities.WaitForUpdates(); + + TestHand rightHand = new TestHand(Handedness.Right); + + yield return rightHand.Show(InputTestUtilities.InFrontOfUser(0.5f)); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // Check if corresponding events were sent + Assert.IsTrue(onFirstHandDetected, "OnFirstHandDetected wasn't successfully sent."); + Assert.IsTrue(onHandActivate, "OnHandActivate wasn't successfully sent."); + + yield return rightHand.Hide(); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // Check if corresponding events were sent + Assert.IsTrue(onHandDeactivate, "OnHandDeactivate wasn't successfully sent."); + Assert.IsTrue(onLastHandLost, "OnLastHandLost wasn't successfully sent."); + + // Reset our state for the second iteration + onFirstHandDetected = false; + onHandActivate = false; + onHandDeactivate = false; + onLastHandLost = false; + + yield return rightHand.Show(new Vector3(-0.05f, -0.05f, 1f)); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // Check if corresponding events were sent + Assert.IsTrue(onFirstHandDetected, "OnFirstHandDetected wasn't successfully sent."); + Assert.IsTrue(onHandActivate, "OnHandActivate wasn't successfully sent."); + + yield return rightHand.Hide(); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // Check if corresponding events were sent + Assert.IsTrue(onHandDeactivate, "OnHandDeactivate wasn't successfully sent."); + Assert.IsTrue(onLastHandLost, "OnLastHandLost wasn't successfully sent."); + } + + /// + /// This checks if the HandConstraint events properly fire when the tracked handedness is set to both hands. + /// + [UnityTest] + public IEnumerator HandConstraintEventsBothHanded() + { + // Disable gaze interactions for this unit test; + InputTestUtilities.DisableGazeInteractor(); + + // Set up GameObject with a SolverHandler + GameObject testObject = GameObject.CreatePrimitive(PrimitiveType.Cube); + SolverHandler solverHandler = testObject.AddComponent(); + + // Set it to track interactors + solverHandler.TrackedHandedness = Handedness.Both; + solverHandler.TrackedTargetType = TrackedObjectType.HandJoint; + solverHandler.TrackedHandJoint = TrackedHandJoint.Palm; + + // Set up GameObject with a HandConstraint + HandConstraint handConstraint = testObject.AddComponent(); + + bool onFirstHandDetected = false; + bool onHandActivate = false; + bool onHandDeactivate = false; + bool onLastHandLost = false; + + handConstraint.OnFirstHandDetected.AddListener(() => onFirstHandDetected = true); + handConstraint.OnHandActivate.AddListener(() => onHandActivate = true); + handConstraint.OnHandDeactivate.AddListener(() => onHandDeactivate = true); + handConstraint.OnLastHandLost.AddListener(() => onLastHandLost = true); + + yield return RuntimeTestUtilities.WaitForUpdates(); + + TestHand rightHand = new TestHand(Handedness.Right); + TestHand leftHand = new TestHand(Handedness.Left); + Vector3 initialHandPosition = InputTestUtilities.InFrontOfUser(0.5f); + + yield return rightHand.Show(initialHandPosition); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // Check if corresponding events were sent + Assert.IsTrue(onFirstHandDetected, "OnFirstHandDetected wasn't successfully sent."); + Assert.IsTrue(onHandActivate, "OnHandActivate wasn't successfully sent."); + + onFirstHandDetected = false; + onHandActivate = false; + + yield return leftHand.Show(new Vector3(-0.05f, -0.05f, 1f)); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // Check if corresponding events were sent (or not) + Assert.IsFalse(onFirstHandDetected, "OnFirstHandDetected should not have been sent."); + Assert.IsFalse(onHandActivate, "OnHandActivate should not have been sent."); + + yield return rightHand.Hide(); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.IsFalse(onFirstHandDetected, "OnFirstHandDetected should not have been sent."); + Assert.IsTrue(onHandDeactivate, "OnHandDeactivate wasn't successfully sent."); + Assert.IsTrue(onHandActivate, "OnHandActivate wasn't successfully sent."); + Assert.IsFalse(onLastHandLost, "OnLastHandLost should not have been sent."); + + onHandDeactivate = false; + onHandActivate = false; + + yield return leftHand.Hide(); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // Check if corresponding events were sent + Assert.IsTrue(onHandDeactivate, "OnHandDeactivate wasn't successfully sent."); + Assert.IsTrue(onLastHandLost, "OnLastHandLost wasn't successfully sent."); + } + } +} +#pragma warning restore CS1591 diff --git a/org.mixedrealitytoolkit.spatialmanipulation/Tests/Runtime/HandConstraintTests.cs.meta b/org.mixedrealitytoolkit.spatialmanipulation/Tests/Runtime/HandConstraintTests.cs.meta new file mode 100644 index 000000000..f358b5ab0 --- /dev/null +++ b/org.mixedrealitytoolkit.spatialmanipulation/Tests/Runtime/HandConstraintTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d25dd7e4a85152f4297bc58762e0ac6e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/org.mixedrealitytoolkit.spatialmanipulation/Tests/Runtime/SolverHandlerTests.cs b/org.mixedrealitytoolkit.spatialmanipulation/Tests/Runtime/SolverHandlerTests.cs index dc0aa7413..aebd28faa 100644 --- a/org.mixedrealitytoolkit.spatialmanipulation/Tests/Runtime/SolverHandlerTests.cs +++ b/org.mixedrealitytoolkit.spatialmanipulation/Tests/Runtime/SolverHandlerTests.cs @@ -4,17 +4,13 @@ // Disable "missing XML comment" warning for tests. While nice to have, this documentation is not required. #pragma warning disable CS1591 -using MixedReality.Toolkit; using MixedReality.Toolkit.Core.Tests; -using MixedReality.Toolkit.Input.Tests; -using MixedReality.Toolkit.Input.Simulation; using MixedReality.Toolkit.Input; +using MixedReality.Toolkit.Input.Tests; using NUnit.Framework; -using System; using System.Collections; using UnityEngine; using UnityEngine.TestTools; -using HandshapeId = MixedReality.Toolkit.Input.HandshapeTypes.HandshapeId; using UnityEngine.XR; namespace MixedReality.Toolkit.SpatialManipulation.Runtime.Tests