Skip to content
1 change: 1 addition & 0 deletions org.mixedrealitytoolkit.spatialmanipulation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,6 @@ public override void SolverUpdate()
return;
}

XRNode? prevTrackedNode = trackedNode;

if (SolverHandler.CurrentTrackedHandedness != Handedness.None)
{
trackedNode = GetControllerNode(SolverHandler.CurrentTrackedHandedness);
Expand Down Expand Up @@ -678,12 +676,11 @@ private bool IsPalmFacingCamera(XRNode? hand)
/// <summary>
/// Returns the XRNode that matches the desired handedness.
/// </summary>
/// <param name="handedness">The handedness of the returned controller</param>
/// <returns>The IMixedRealityController for the desired handedness, or null if none are present.</returns>
/// <param name="handedness">The handedness of the returned controller node.</param>
/// <returns>The <see cref="XRNode"/> for the desired handedness, or null if none are present.</returns>
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;

/// <summary>
/// Currently tracked hand or motion controller, if applicable.
/// </summary>
/// <remarks>
/// The allowable <see cref="Handedness"/> values are Left, Right, or None
/// The allowable <see cref="Handedness"/> values are Left, Right, or None.
/// </remarks>
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;
Expand Down Expand Up @@ -424,7 +420,7 @@ internal void UnregisterSolver(Solver solver)
}

/// <summary>
/// Clear the parent of the internally created tracking-target game object.
/// Clear the parent of the internally created tracking-target game object.
/// </summary>
/// <remarks>
/// A tracking-target is created when <see cref="AttachToNewTrackedObject"/> is called, and
Expand Down Expand Up @@ -452,7 +448,7 @@ protected virtual void AttachToNewTrackedObject()
{
using (AttachToNewTrackedObjectPerfMarker.Auto())
{
currentTrackedHandedness = Handedness.None;
CurrentTrackedHandedness = Handedness.None;
controllerInteractor = null;

Transform target = null;
Expand All @@ -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;
Expand All @@ -518,7 +512,7 @@ protected virtual void AttachToNewTrackedObject()
}
else if (TrackedTargetType == TrackedObjectType.CustomOverride)
{
target = this.transformOverride;
target = transformOverride;
}

TrackTransform(target);
Expand All @@ -529,7 +523,7 @@ protected virtual void AttachToNewTrackedObject()
new ProfilerMarker("[MRTK] SolverHandler.UpdateCachedHandJointTransform");

/// <summary>
/// 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.
/// </summary>
/// <returns>
/// <see langword="true"/> if the tracked joint is found and cached transform is updated, <see langword="false"/> otherwise.
Expand All @@ -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 &&
Expand Down Expand Up @@ -571,7 +565,6 @@ private void TrackTransform(Transform target)
trackingTarget.transform.localPosition = Vector3.Scale(AdditionalOffset, trackingTarget.transform.localScale);
trackingTarget.transform.localRotation = Quaternion.Euler(AdditionalRotation);
}

}

/// <summary>
Expand Down Expand Up @@ -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;
}

Expand All @@ -630,14 +623,14 @@ private bool IsInvalidTracking()
/// <returns>
/// <see langword="true"/> if the hand is tracked, or <see langword="false"/>.
/// </returns>
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 _);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Tests for <see cref="HandConstraint"/>.
/// </summary>
public class HandConstraintTests : BaseRuntimeInputTests
{
/// <summary>
/// This checks if the HandConstraint events properly fire when the tracked handedness is set to a single hand.
/// </summary>
[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<SolverHandler>();

// 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<HandConstraint>();

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.");
}

/// <summary>
/// This checks if the HandConstraint events properly fire when the tracked handedness is set to both hands.
/// </summary>
[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<SolverHandler>();

// 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<HandConstraint>();

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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading