Skip to content

Commit

Permalink
Implement unified logic
Browse files Browse the repository at this point in the history
  • Loading branch information
ahmed-shariff committed Jan 9, 2024
1 parent a9b3f64 commit 348479b
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 15 deletions.
1 change: 1 addition & 0 deletions Runtime/Interaction/HPUIEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace ubco.ovilab.HPUI.Interaction
{
public enum HPUIGestureState
{
None,
Tap, Swipe,
Custom // TODO: Custom gestures?
}
Expand Down
8 changes: 8 additions & 0 deletions Runtime/Interaction/Logic.meta

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 @@ -9,7 +9,7 @@ namespace ubco.ovilab.HPUI.Interaction
/// <summary>
/// Encapsulates the logic for HPUI gesture interactions.
/// </summary>
public class HPUIGestureLogic: IDisposable
public class HPUIGestureLogicDistributed: IHPUIGestureLogic
{
private Dictionary<IHPUIInteractable, HPUIInteractionState> states = new Dictionary<IHPUIInteractable, HPUIInteractionState>();
private float previousTime;
Expand Down Expand Up @@ -147,22 +147,23 @@ public void Dispose()
hpuiSwipeEventArgsPool.Dispose();
states.Clear();
}
}

public class HPUIInteractionState
{
public HPUIGestureState gestureState;
public float startTime;
public Vector2 startPosition;
public Vector2 previousPosition;

public HPUIInteractionState SetParams(HPUIGestureState gestureState, float startTime, Vector2 startPosition)
class HPUIInteractionState
{
this.gestureState = gestureState;
this.startTime = startTime;
this.startPosition = startPosition;
this.previousPosition = startPosition;
return this;
public HPUIGestureState gestureState;
public float startTime;
public Vector2 startPosition;
public Vector2 previousPosition;

public HPUIInteractionState SetParams(HPUIGestureState gestureState, float startTime, Vector2 startPosition)
{
this.gestureState = gestureState;
this.startTime = startTime;
this.startPosition = startPosition;
this.previousPosition = startPosition;
return this;
}
}
}

}
248 changes: 248 additions & 0 deletions Runtime/Interaction/Logic/HPUIGestureLogicUnified.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.XR.Interaction.Toolkit;

namespace ubco.ovilab.HPUI.Interaction
{
/// <summary>
/// Encapsulates the logic for HPUI gesture interactions.
/// The interactor dectates which interactable gets the gesture.
/// </summary>
public class HPUILogicUnified: IHPUIGestureLogic
{
// private Dictionary<IHPUIInteractable, HPUIInteractionState> states = new Dictionary<IHPUIInteractable, HPUIInteractionState>();

private LinkedPool<HPUITapEventArgs> hpuiTapEventArgsPool = new LinkedPool<HPUITapEventArgs>(() => new HPUITapEventArgs());
private LinkedPool<HPUISwipeEventArgs> hpuiSwipeEventArgsPool = new LinkedPool<HPUISwipeEventArgs>(() => new HPUISwipeEventArgs());

private float tapTimeThreshold, tapDistanceThreshold;
private IHPUIInteractor interactor;

private float startTime, distance;
private Vector2 previousPosition, cumilativeDelta;

private int activePriorityZIndex = int.MaxValue;
private int activeInteractablesCount = 0;
private bool changedInteractable = false;

private IHPUIInteractable activePriorityInteractable, currentTrackingInteractable;
private Dictionary<IHPUIInteractable, HPUIInteractionState> activeInteractables = new Dictionary<IHPUIInteractable, HPUIInteractionState>();

private HPUIGestureState interactorGestureState = HPUIGestureState.None;

/// <summary>
/// Initializes a new instance of the with the thrshold values.
/// </summary>
public HPUILogicUnified(IHPUIInteractor interactor, float tapTimeThreshold, float tapDistanceThreshold)
{
this.interactor = interactor;
this.tapTimeThreshold = tapTimeThreshold;
this.tapDistanceThreshold = tapDistanceThreshold;
Reset();
}

/// <inheritdoc />
public void OnSelectEntering(IHPUIInteractable interactable)
{
if (interactable == null)
{
return;
}

activeInteractablesCount += 1;

if (interactorGestureState == HPUIGestureState.None)
{
interactorGestureState = HPUIGestureState.Tap;
startTime = Time.time;
}

if (activeInteractables.ContainsKey(interactable))
{
activeInteractables[interactable].active = true;
}
else
{
HPUIInteractionState state = new HPUIInteractionState(Time.time);
activeInteractables.Add(interactable, state);

float currentTime = Time.time;

// If a new higher priority targets is encountered within tap time window, we hand over control to that.
if (interactable.zOrder < activePriorityZIndex && (currentTime - startTime) < tapTimeThreshold)
{
activePriorityZIndex = interactable.zOrder;
activePriorityInteractable = interactable;

foreach(HPUIInteractionState interactionState in activeInteractables.Values)
{
interactionState.startTime = currentTime;
}
}
}
ComputeCurrectTrackingInteractable();
}

private void ComputeCurrectTrackingInteractable()
{
if (activePriorityInteractable == currentTrackingInteractable)
{
return;
}

IHPUIInteractable interactableToTrack = activeInteractables
.Where(kvp => kvp.Value.active)
.OrderBy(kvp => kvp.Key.zOrder)
.ThenBy(kvp => kvp.Value.startTime)
.First().Key;

if (interactableToTrack != currentTrackingInteractable)
{
currentTrackingInteractable = interactableToTrack;
changedInteractable = true;
}
}

/// <inheritdoc />
public void OnSelectExiting(IHPUIInteractable interactable)
{
if (interactable == null)
{
return;
}

activeInteractablesCount -= 1;

if (activeInteractablesCount == 0)
{
switch (interactorGestureState)
{
case HPUIGestureState.Tap:
using (hpuiTapEventArgsPool.Get(out HPUITapEventArgs tapEventArgs))
{
tapEventArgs.SetParams(interactor, activePriorityInteractable);
activePriorityInteractable.OnTap(tapEventArgs);
interactor.OnTap(tapEventArgs);
}
break;
case HPUIGestureState.Swipe:
using (hpuiSwipeEventArgsPool.Get(out HPUISwipeEventArgs swipeEventArgs))
{
swipeEventArgs.SetParams(interactor, activePriorityInteractable,
HPUISwipeState.Stopped, Time.time - state.startTime, state.startTime, state.startPosition,
state.previousPosition, state.previousPosition, state.previousPosition - state.startPosition);
activePriorityInteractable.OnSwipe(swipeEventArgs);
interactor.OnSwipe(swipeEventArgs);

}
break;
}

Reset();
}
else
{
if (activeInteractables.ContainsKey(interactable))
{
activeInteractables[interactable].active = false;
ComputeCurrectTrackingInteractable();
}
}
}

protected void Reset()
{
activeInteractablesCount = 0;
interactorGestureState = HPUIGestureState.None;
activeInteractables.Clear();
activePriorityInteractable = null;
activePriorityZIndex = int.MaxValue;
currentTrackingInteractable = null;
distance = 0;
cumilativeDelta = Vector2.zero;
}

/// <inheritdoc />
public void Update()
{
if (interactorGestureState == HPUIGestureState.None)
{
return;
}

// If interactable change, we need to restart tracking, hence skipping a frame
if (changedInteractable)
{
// FIXME: an event should be triggered here?
previousPosition = currentTrackingInteractable.ComputeInteractorPostion(interactor);
changedInteractable = false;
return;
}

Vector2 currentPosition = currentTrackingInteractable.ComputeInteractorPostion(interactor);
Vector2 delta = previousPosition - currentPosition;
float timeDelta = Time.time - startTime;
distance += delta.magnitude;
cumilativeDelta += delta;

switch(interactorGestureState)
{
case HPUIGestureState.Tap:
if (timeDelta > tapTimeThreshold || distance > tapDistanceThreshold)
{
using (hpuiSwipeEventArgsPool.Get(out HPUISwipeEventArgs swipeEventArgs))
{
swipeEventArgs.SetParams(interactor, activePriorityInteractable,
HPUISwipeState.Started, timeDelta, state.startTime, state.startPosition, state.previousPosition,
currentPosition, direction);
activePriorityInteractable.OnSwipe(swipeEventArgs);
interactor.OnSwipe(swipeEventArgs);
}
}
break;
case HPUIGestureState.Swipe:
using (hpuiSwipeEventArgsPool.Get(out HPUISwipeEventArgs swipeEventArgs))
{
swipeEventArgs.SetParams(interactor, activePriorityInteractable,
HPUISwipeState.Updated, timeDelta, state.startTime, state.startPosition, state.previousPosition,
currentPosition, direction);
activePriorityInteractable.OnSwipe(swipeEventArgs);
interactor.OnSwipe(swipeEventArgs);
}
break;
case HPUIGestureState.Custom:
// TODO: custom gestures
throw new NotImplementedException();
}

}

/// <summary>
/// Clear cached objects.
/// </summary>
public void Dispose()
{
Reset();
hpuiTapEventArgsPool.Dispose();
hpuiSwipeEventArgsPool.Dispose();
}

class HPUIInteractionState
{
public float startTime;
// public Vector2 startPosition;
// public Vector2 previousPosition;
public bool active;

public HPUIInteractionState(float startTime)
{
this.startTime = startTime;
this.active = true;
}
}
}

}
11 changes: 11 additions & 0 deletions Runtime/Interaction/Logic/HPUIGestureLogicUnified.cs.meta

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

21 changes: 21 additions & 0 deletions Runtime/Interaction/Logic/IHPUIGestureLogic.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace ubco.ovilab.HPUI.Interaction
{
public interface IHPUIGestureLogic: IDisposable
{
/// <summary>
/// To be called by <see cref="IXRSelectInteractor.OnSelectEntering"/> controlling this <see cref="HPUIGestureLogic"/>
/// </summary>
public void OnSelectEntering(IHPUIInteractable interactable);

/// <summary>
/// To be called by <see cref="IXRSelectInteractor.OnSelectExiting"/> controlling this <see cref="HPUIGestureLogic"/>
/// </summary>
public void OnSelectExiting(IHPUIInteractable interactable);

/// <summary>
/// Updat method to be called from an interactor. Updates the states of the <see cref="IHPUIInteractable"/> selected by
/// the <see cref="IXRInteractor"/> that was passed when initializing this <see cref="HPUIGestureLogic"/>.
/// </summary>
public void Update();
}
}
11 changes: 11 additions & 0 deletions Runtime/Interaction/Logic/IHPUIGestureLogic.cs.meta

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

0 comments on commit 348479b

Please sign in to comment.