Skip to content

Commit

Permalink
Adding IsProximityHovered property of type TimedFlag to detect when a…
Browse files Browse the repository at this point in the history
… button starts being hovered or on interactor proximity and when it stops being hovered or on proximity of any interactor. (#611)

Adding ProximityHover events (Entered & Exited) to PressableButton class.
  • Loading branch information
ms-RistoRK authored Feb 14, 2024
1 parent 60a315d commit bbdc68f
Show file tree
Hide file tree
Showing 14 changed files with 357 additions and 10 deletions.
1 change: 1 addition & 0 deletions org.mixedrealitytoolkit.core/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
[assembly: AssemblyCopyright("Copyright (c) Mixed Reality Toolkit Contributors")]

[assembly: InternalsVisibleTo("MixedReality.Toolkit.Input")]
[assembly: InternalsVisibleTo("MixedReality.Toolkit.UXCore")]

// The AssemblyVersion attribute is checked-in and is recommended not to be changed often.
// https://docs.microsoft.com/troubleshoot/visualstudio/general/assembly-version-assembly-file-version
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

namespace MixedReality.Toolkit.Input
{
/// <summary>
/// Event data associated with proximity events triggered by a Collider and an Interactor combo.
/// </summary>
public abstract partial class BaseProximityEventArgs : EventArgs
{
/// <summary>
/// Constructor for BaseProximityEventArgs.
/// </summary>
/// <param name="sender">Source of event.</param>
/// <param name="collider">Collider that triggers proximity event.</param>
/// <param name="interactor">XRBaseInteractor that triggers proximity event.</param>
public BaseProximityEventArgs(object sender, Collider collider, XRBaseInteractor interactor)
{
this.sender = sender;
this.collider = collider;
this.interactor = interactor;
}

/// <summary>
/// The object that triggered the proximity event.
/// </summary>
public object sender { get; private set; }

/// <summary>
/// The collider associated with the interaction event.
/// </summary>
public Collider collider { get; private set; }

/// <summary>
/// The interactor associated with the interaction event.
/// </summary>
public XRBaseInteractor interactor { get; private set; }
}
}

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Mixed Reality Toolkit Contributors
// Licensed under the BSD 3-Clause

using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

namespace MixedReality.Toolkit.Input
{
/// <summary>
/// This interface is used to update the enable state of the Components that are in the <see cref="ProximityEnabledComponents"/>
/// array (set in Editor) and to keep track of which <see cref="Collider"/> and <see cref="XRBaseInteractor"/> duples are triggering proximity.
/// </summary>
/// <remarks>
/// This interface is needed to prevent a circular reference between MRTK Input and MRTK UX Core Scripts packages.
/// </remarks>
public interface IXRProximityInteractable
{
/// <summary>
/// Registers the duple Collider + XRBaseInteractor as triggering proximity.
/// </summary>
/// <param name="collider">Collider triggering proximity.</param>
/// <param name="xrBaseInteractor">Interactor triggering proximity.</param>
void OnProximityEntered(ProximityEnteredEventArgs args);

/// <summary>
/// Unregisters the duple Collider + XRBaseInteractor as triggering proximity.
/// </summary>
/// <param name="collider">Collider that in combination with the interactor was triggering proximity.</param>
/// <param name="xrBaseInteractor">Interactor that in combination with the collider was triggering proximity.</param>
void OnProximityExited(ProximityExitedEventArgs args);
}
}

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

Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,52 @@ public class NearInteractionModeDetector : ProximityDetector
[Tooltip("The set of near interactors that belongs to near interaction")]
private List<XRBaseInteractor> nearInteractors;

/// <summary>
/// Used to keep track of the previously detected colliders so that we can know which
/// colliders stopped being detected and update their buttons front plate and rounded
/// rect if they are labeled as dynamic (based on proximity).
/// </summary>
List<Collider> previouslyDetectedColliders = new List<Collider>();

/// <inheritdoc />
public override bool IsModeDetected()
{
return base.IsModeDetected() || IsNearInteractorSelecting();
bool result = base.IsModeDetected() || IsNearInteractorSelecting();
if (result)
{
for (int i = 0; i < previouslyDetectedColliders.Count; i++)
{
Collider previouslyDetectedCollider = previouslyDetectedColliders[i];
if (!DetectedColliders.Contains(previouslyDetectedCollider) && previouslyDetectedCollider != null)
{
IXRProximityInteractable nearInteractionMode = previouslyDetectedCollider.GetComponent<IXRProximityInteractable>();
if (nearInteractionMode != null)
{
foreach (XRBaseInteractor xrBaseInteractor in nearInteractors)
{
previouslyDetectedCollider.GetComponent<IXRProximityInteractable>().OnProximityExited(new ProximityExitedEventArgs(this, previouslyDetectedCollider, xrBaseInteractor));
}
}
previouslyDetectedColliders.Remove(previouslyDetectedCollider);
}
}
foreach (Collider collider in DetectedColliders)
{
if (!previouslyDetectedColliders.Contains(collider))
{
IXRProximityInteractable nearInteractionMode = collider.GetComponent<IXRProximityInteractable>();
if (nearInteractionMode != null)
{
foreach (XRBaseInteractor xrBaseInteractor in nearInteractors)
{
collider.GetComponent<IXRProximityInteractable>().OnProximityEntered(new ProximityEnteredEventArgs(this, collider, xrBaseInteractor));
}
}
previouslyDetectedColliders.Add(collider);
}
}
}
return result;
}

private bool IsNearInteractorSelecting()
Expand All @@ -38,4 +80,4 @@ private bool IsNearInteractorSelecting()
return false;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

namespace MixedReality.Toolkit.Input
{
public class ProximityEnteredEventArgs : BaseProximityEventArgs
{
/// <summary>
/// Constructor for ProximityEnteredArgs.
/// </summary>
/// <param name="sender">Source of event.</param>
/// <param name="collider">Collider associated with the Proximity Entered event.</param>
/// <param name="interactor">Interactor associated with the Proximity Entered event.</param>
public ProximityEnteredEventArgs(object sender, Collider collider, XRBaseInteractor interactor) : base(sender, collider, interactor)
{
//Empty on purpose
}

/// <summary>
/// The collider associated with the proximity entered event.
/// </summary>
public new Collider collider => base.collider;

/// <summary>
/// The Interactor associated with the proximity entered event.
/// </summary>
public new XRBaseInteractor interactor => base.interactor;
}
}

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

namespace MixedReality.Toolkit.Input
{
public class ProximityExitedEventArgs : BaseProximityEventArgs
{
/// <summary>
/// Constructor for ProximityExitedArgs.
/// </summary>
/// <param name="sender">Source of event.</param>
/// <param name="collider">Collider associated with the Proximity Exited event.</param>
/// <param name="interactor">Interactor associated with the Proximity Exited event.</param>
public ProximityExitedEventArgs(object sender, Collider collider, XRBaseInteractor interactor) : base(sender, collider, interactor)
{
//Empty on purpose
}

/// <summary>
/// The Collider associated with the proximity exited event.
/// </summary>
public new Collider collider => base.collider;

/// <summary>
/// The Interactor associated with the proximity exited event.
/// </summary>
public new XRBaseInteractor interactor => base.interactor;
}
}

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

Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
using MixedReality.Toolkit.Core.Tests;
using MixedReality.Toolkit.Input.Tests;
using NUnit.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using UnityEngine.TestTools;

using HandshapeId = MixedReality.Toolkit.Input.HandshapeTypes.HandshapeId;
using Object = UnityEngine.Object;
using SpaceMode = MixedReality.Toolkit.UX.PressableButton.SpaceMode;

namespace MixedReality.Toolkit.UX.Runtime.Tests
Expand Down Expand Up @@ -850,6 +853,61 @@ public IEnumerator ChangeSpeechSettingsDisabled([ValueSource(nameof(PressableBut
// Wait for a frame to give Unity a chance to actually destroy the object
yield return null;
}

/// <summary>
/// Test the PressableButton script has the activeCollidersWithInteractor hashset.
/// </summary>
[UnityTest]
public IEnumerator TestPressableButtonHasActiveCollidersWithInteractorHashset()
{
FieldInfo[] fieldInfos;
System.Type pressableButtonType = typeof(PressableButton);

fieldInfos = pressableButtonType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
var result = fieldInfos.Where(fieldInfo => fieldInfo.Name.Equals("activeCollidersWithInteractor")).ToArray();

Assert.AreEqual(1, result.Length);

yield return null;
}

/// <summary>
/// Tests PressableButton has IsProximityHovered property
/// </summary>
[UnityTest]
public IEnumerator TestPressableButtonHasIsProximityHoveredProperty([ValueSource(nameof(PressableButtonsTestPrefabPaths))] string prefabFilename)
{
// instantiate scene and button
GameObject testButton = InstantiateDefaultPressableButton(prefabFilename);
yield return null;

PressableButton buttonComponent = testButton.GetComponent<PressableButton>();
Assert.IsNotNull(buttonComponent.IsProximityHovered);

Object.Destroy(testButton);
// Wait for a frame to give Unity a change to actually destroy the object

yield return null;
}

/// <summary>
/// Tests PressableButton IsProximityHovered property is of type TimedFlag
/// </summary>
[UnityTest]
public IEnumerator TestPressableButtonIsProximityHoveredPropertyIsOfTypeTimedFlag([ValueSource(nameof(PressableButtonsTestPrefabPaths))] string prefabFilename)
{
// instantiate scene and button
GameObject testButton = InstantiateDefaultPressableButton(prefabFilename);
yield return null;

PressableButton buttonComponent = testButton.GetComponent<PressableButton>();
Assert.AreEqual(typeof(TimedFlag), buttonComponent.IsProximityHovered.GetType());

Object.Destroy(testButton);
// Wait for a frame to give Unity a change to actually destroy the object

yield return null;
}
#endregion Tests

#region Private methods
Expand Down Expand Up @@ -912,4 +970,4 @@ private IEnumerator ReleaseButtonWithHand(Vector3 buttonPosition, bool doRolloff
#endregion Private methods
}
}
#pragma warning restore CS1591
#pragma warning restore CS1591
Loading

0 comments on commit bbdc68f

Please sign in to comment.