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