685 lines
24 KiB
C#
685 lines
24 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
using Microsoft.MixedReality.Toolkit.Physics;
|
|
using System;
|
|
using System.Collections;
|
|
using Unity.Profiling;
|
|
using UnityEngine;
|
|
|
|
namespace Microsoft.MixedReality.Toolkit.Input
|
|
{
|
|
/// <summary>
|
|
/// Base Pointer class for pointers that exist in the scene as GameObjects.
|
|
/// </summary>
|
|
[DisallowMultipleComponent]
|
|
[HelpURL("https://docs.microsoft.com/windows/mixed-reality/mrtk-unity/features/input/pointers")]
|
|
public abstract class BaseControllerPointer : ControllerPoseSynchronizer, IMixedRealityQueryablePointer
|
|
{
|
|
[SerializeField]
|
|
private GameObject cursorPrefab = null;
|
|
|
|
[SerializeField]
|
|
private bool disableCursorOnStart = false;
|
|
|
|
protected bool DisableCursorOnStart => disableCursorOnStart;
|
|
|
|
[SerializeField]
|
|
private bool setCursorVisibilityOnSourceDetected = false;
|
|
|
|
private GameObject cursorInstance = null;
|
|
|
|
[SerializeField]
|
|
[Tooltip("Source transform for raycast origin - leave null to use default transform")]
|
|
protected Transform raycastOrigin = null;
|
|
|
|
[SerializeField]
|
|
[Tooltip("The hold action that will enable the raise the input event for this pointer.")]
|
|
private MixedRealityInputAction activeHoldAction = MixedRealityInputAction.None;
|
|
|
|
[SerializeField]
|
|
[Tooltip("The action that will enable the raise the input event for this pointer.")]
|
|
protected MixedRealityInputAction pointerAction = MixedRealityInputAction.None;
|
|
|
|
[SerializeField]
|
|
[Tooltip("The action that will enable the raise the input grab event for this pointer.")]
|
|
protected MixedRealityInputAction grabAction = MixedRealityInputAction.None;
|
|
|
|
/// <summary>
|
|
/// True if grab is pressed right now
|
|
/// </summary>
|
|
protected bool IsGrabPressed = false;
|
|
|
|
[SerializeField]
|
|
[Tooltip("Does the interaction require hold?")]
|
|
private bool requiresHoldAction = false;
|
|
|
|
[SerializeField]
|
|
[Tooltip("Does the interaction require the action to occur at least once first?")]
|
|
private bool requiresActionBeforeEnabling = true;
|
|
|
|
/// <summary>
|
|
/// True if select is pressed right now
|
|
/// </summary>
|
|
protected bool IsSelectPressed = false;
|
|
|
|
/// <summary>
|
|
/// True if select has been pressed once since this component was enabled
|
|
/// </summary>
|
|
protected bool HasSelectPressedOnce = false;
|
|
|
|
protected bool IsHoldPressed = false;
|
|
|
|
private bool isCursorInstantiatedFromPrefab = false;
|
|
|
|
private static readonly ProfilerMarker SetCursorPerfMarker = new ProfilerMarker("[MRTK] BaseControllerPointer.SetCursor");
|
|
|
|
/// <summary>
|
|
/// Set a new cursor for this <see cref="Microsoft.MixedReality.Toolkit.Input.IMixedRealityPointer"/>
|
|
/// </summary>
|
|
/// <remarks>This <see href="https://docs.unity3d.com/ScriptReference/GameObject.html">GameObject</see> must have a <see cref="Microsoft.MixedReality.Toolkit.Input.IMixedRealityCursor"/> attached to it.</remarks>
|
|
/// <param name="newCursor">The new cursor</param>
|
|
public virtual void SetCursor(GameObject newCursor = null)
|
|
{
|
|
using (SetCursorPerfMarker.Auto())
|
|
{
|
|
// Destroy the old cursor and replace it with the new one if a new cursor was provided
|
|
if (cursorInstance != null && newCursor != null)
|
|
{
|
|
DestroyCursorInstance();
|
|
cursorInstance = newCursor;
|
|
}
|
|
|
|
if (cursorInstance == null && cursorPrefab != null)
|
|
{
|
|
// We spawn the cursor at the same level as this pointer by setting its parent to be the same as the pointer's
|
|
// In the future, the pointer will not be responsible for instantiating the cursor, so we'll avoid making this assumption about the hierarchy
|
|
cursorInstance = Instantiate(cursorPrefab, transform.parent);
|
|
isCursorInstantiatedFromPrefab = true;
|
|
}
|
|
|
|
if (cursorInstance != null)
|
|
{
|
|
cursorInstance.name = $"{name}_Cursor";
|
|
|
|
BaseCursor oldC = BaseCursor as BaseCursor;
|
|
if (oldC != null && enabled)
|
|
{
|
|
oldC.VisibleSourcesCount--;
|
|
}
|
|
|
|
BaseCursor = cursorInstance.GetComponent<IMixedRealityCursor>();
|
|
|
|
BaseCursor newC = BaseCursor as BaseCursor;
|
|
if (newC != null && enabled)
|
|
{
|
|
newC.VisibleSourcesCount++;
|
|
}
|
|
|
|
if (BaseCursor != null)
|
|
{
|
|
BaseCursor.DefaultCursorDistance = DefaultPointerExtent;
|
|
BaseCursor.Pointer = this;
|
|
BaseCursor.SetVisibilityOnSourceDetected = setCursorVisibilityOnSourceDetected;
|
|
|
|
if (disableCursorOnStart)
|
|
{
|
|
BaseCursor.SetVisibility(false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError($"No IMixedRealityCursor component found on {cursorInstance.name}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DestroyCursorInstance()
|
|
{
|
|
if (cursorInstance != null)
|
|
{
|
|
// Destroy correctly depending on if in play mode or edit mode
|
|
GameObjectExtensions.DestroyGameObject(cursorInstance);
|
|
}
|
|
}
|
|
|
|
#region MonoBehaviour Implementation
|
|
|
|
protected override void OnEnable()
|
|
{
|
|
base.OnEnable();
|
|
|
|
// Disable renderers so that they don't display before having been processed (which manifests as a flash at the origin).
|
|
var renderers = GetComponentsInChildren<Renderer>();
|
|
if (renderers != null)
|
|
{
|
|
foreach (var renderer in renderers)
|
|
{
|
|
renderer.enabled = false;
|
|
}
|
|
}
|
|
|
|
SetCursor();
|
|
}
|
|
|
|
protected override async void Start()
|
|
{
|
|
base.Start();
|
|
|
|
await EnsureInputSystemValid();
|
|
|
|
// We've been destroyed during the await.
|
|
if (this.IsNull())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// The pointer's input source was lost during the await.
|
|
if (Controller == null)
|
|
{
|
|
GameObjectExtensions.DestroyGameObject(gameObject);
|
|
return;
|
|
}
|
|
}
|
|
|
|
protected override void OnDisable()
|
|
{
|
|
if (IsSelectPressed || IsGrabPressed)
|
|
{
|
|
CoreServices.InputSystem?.RaisePointerUp(this, pointerAction, Handedness);
|
|
}
|
|
|
|
base.OnDisable();
|
|
|
|
IsHoldPressed = false;
|
|
IsSelectPressed = false;
|
|
IsGrabPressed = false;
|
|
HasSelectPressedOnce = false;
|
|
BaseCursor?.SetVisibility(false);
|
|
|
|
BaseCursor c = BaseCursor as BaseCursor;
|
|
if (c != null)
|
|
{
|
|
c.VisibleSourcesCount--;
|
|
}
|
|
|
|
// Need to destroy instantiated cursor prefab if it was added by the controller itself in 'OnEnable'
|
|
if (isCursorInstantiatedFromPrefab)
|
|
{
|
|
// Manually reset base cursor before destroying it
|
|
BaseCursor?.Destroy();
|
|
DestroyCursorInstance();
|
|
isCursorInstantiatedFromPrefab = false;
|
|
}
|
|
}
|
|
|
|
#endregion MonoBehaviour Implementation
|
|
|
|
#region IMixedRealityPointer Implementation
|
|
|
|
/// <inheritdoc />
|
|
public override IMixedRealityController Controller
|
|
{
|
|
get => base.Controller;
|
|
set
|
|
{
|
|
base.Controller = value;
|
|
|
|
if (base.Controller != null && this.IsNotNull())
|
|
{
|
|
// Ensures that the basePointerName is only initialized once
|
|
if (basePointerName == string.Empty)
|
|
{
|
|
basePointerName = gameObject.name;
|
|
}
|
|
PointerName = $"{Handedness}_{basePointerName}";
|
|
SetCursor();
|
|
}
|
|
}
|
|
}
|
|
|
|
private uint pointerId;
|
|
|
|
/// <inheritdoc />
|
|
public uint PointerId
|
|
{
|
|
get
|
|
{
|
|
if (pointerId == 0)
|
|
{
|
|
pointerId = CoreServices.InputSystem.FocusProvider.GenerateNewPointerId();
|
|
}
|
|
|
|
return pointerId;
|
|
}
|
|
}
|
|
|
|
private string basePointerName = string.Empty;
|
|
private string pointerName = string.Empty;
|
|
|
|
/// <inheritdoc />
|
|
public string PointerName
|
|
{
|
|
get => pointerName;
|
|
set
|
|
{
|
|
pointerName = value;
|
|
if (this.IsNotNull())
|
|
{
|
|
gameObject.name = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IMixedRealityInputSource InputSourceParent
|
|
{
|
|
get { return base.Controller?.InputSource; }
|
|
|
|
#if UNITY_2020_3_OR_NEWER
|
|
[Obsolete("Setting the Input Source Parent directly is no longer supported")]
|
|
#endif
|
|
protected set { Debug.LogWarning("Setting the Input Source Parent directly is no longer supported"); }
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IMixedRealityCursor BaseCursor { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public ICursorModifier CursorModifier { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public virtual bool IsInteractionEnabled
|
|
{
|
|
get
|
|
{
|
|
if (IsFocusLocked)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!IsActive)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (requiresHoldAction && IsHoldPressed)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (IsSelectPressed || IsGrabPressed)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return HasSelectPressedOnce || !requiresActionBeforeEnabling;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual bool IsActive { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public bool IsFocusLocked { get; set; }
|
|
|
|
/// <summary>
|
|
/// Specifies whether the pointer's target position (cursor) is locked to the target object when focus is locked.
|
|
/// Most pointers want the cursor to "stick" to the object when manipulating, so set this to true by default.
|
|
/// </summary>
|
|
public virtual bool IsTargetPositionLockedOnFocusLock { get; set; } = true;
|
|
|
|
[SerializeField]
|
|
private bool overrideGlobalPointerExtent = false;
|
|
|
|
[SerializeField]
|
|
[Tooltip("Maximum distance at which all pointers can collide with a GameObject, unless it has an override extent.")]
|
|
private float pointerExtent = 10f;
|
|
|
|
/// <summary>
|
|
/// Maximum distance at which all pointers can collide with a <see href="https://docs.unity3d.com/ScriptReference/GameObject.html">GameObject</see>, unless it has an override extent.
|
|
/// </summary>
|
|
public float PointerExtent
|
|
{
|
|
get
|
|
{
|
|
if (overrideGlobalPointerExtent)
|
|
{
|
|
if (CoreServices.InputSystem?.FocusProvider != null)
|
|
{
|
|
return CoreServices.InputSystem.FocusProvider.GlobalPointingExtent;
|
|
}
|
|
}
|
|
|
|
return pointerExtent;
|
|
}
|
|
set
|
|
{
|
|
pointerExtent = value;
|
|
overrideGlobalPointerExtent = false;
|
|
}
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("The length of the pointer when nothing is hit")]
|
|
private float defaultPointerExtent = 10f;
|
|
|
|
/// <summary>
|
|
/// The length of the pointer when nothing is hit.
|
|
/// </summary>
|
|
public float DefaultPointerExtent
|
|
{
|
|
get => Mathf.Min(defaultPointerExtent, PointerExtent);
|
|
set => defaultPointerExtent = value;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public RayStep[] Rays { get; protected set; } = { new RayStep(Vector3.zero, Vector3.forward) };
|
|
|
|
/// <inheritdoc />
|
|
public virtual LayerMask[] PrioritizedLayerMasksOverride { get; set; } = null;
|
|
|
|
/// <inheritdoc />
|
|
public IMixedRealityFocusHandler FocusTarget { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public IPointerResult Result { get; set; }
|
|
|
|
/// <summary>
|
|
/// Ray stabilizer used when calculating position of pointer end point.
|
|
/// </summary>
|
|
public IBaseRayStabilizer RayStabilizer { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public virtual SceneQueryType SceneQueryType { get; set; } = SceneQueryType.SimpleRaycast;
|
|
|
|
[SerializeField]
|
|
[Tooltip("How far controller needs to be from object before object can be grabbed / focused.")]
|
|
private float sphereCastRadius = 0.1f;
|
|
|
|
/// <inheritdoc />
|
|
public float SphereCastRadius
|
|
{
|
|
get => sphereCastRadius;
|
|
set => sphereCastRadius = value;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual Vector3 Position => raycastOrigin != null ? raycastOrigin.position : transform.position;
|
|
|
|
/// <inheritdoc />
|
|
public virtual Quaternion Rotation => raycastOrigin != null ? raycastOrigin.rotation : transform.rotation;
|
|
|
|
/// <inheritdoc />
|
|
public virtual void OnPreSceneQuery() { }
|
|
|
|
/// <inheritdoc />
|
|
public virtual bool OnSceneQuery(LayerMask[] prioritizedLayerMasks, bool focusIndividualCompoundCollider, out MixedRealityRaycastHit hitInfo, out RayStep Ray, out int rayStepIndex)
|
|
{
|
|
float rayStartDistance = 0;
|
|
var raycastProvider = CoreServices.InputSystem.RaycastProvider;
|
|
|
|
switch (SceneQueryType)
|
|
{
|
|
case SceneQueryType.SimpleRaycast:
|
|
for (int i = 0; i < Rays.Length; i++)
|
|
{
|
|
if (raycastProvider.Raycast(Rays[i], prioritizedLayerMasks, focusIndividualCompoundCollider, out hitInfo))
|
|
{
|
|
// Ensure that our distance is the sum of the rays we've traversed so far
|
|
hitInfo.distance += rayStartDistance;
|
|
Ray = Rays[i];
|
|
rayStepIndex = i;
|
|
return true;
|
|
}
|
|
rayStartDistance += Rays[i].Length;
|
|
}
|
|
break;
|
|
case SceneQueryType.SphereCast:
|
|
for (int i = 0; i < Rays.Length; i++)
|
|
{
|
|
if (raycastProvider.SphereCast(Rays[i], SphereCastRadius, prioritizedLayerMasks, focusIndividualCompoundCollider, out hitInfo))
|
|
{
|
|
// Ensure that our distance is the sum of the rays we've traversed so far
|
|
hitInfo.distance += rayStartDistance;
|
|
Ray = Rays[i];
|
|
rayStepIndex = i;
|
|
return true;
|
|
}
|
|
rayStartDistance += Rays[i].Length;
|
|
}
|
|
break;
|
|
default:
|
|
throw new System.Exception("The Base Controller Pointer does not handle non-raycast scene queries");
|
|
}
|
|
|
|
hitInfo = new MixedRealityRaycastHit();
|
|
Ray = Rays[0];
|
|
rayStepIndex = 0;
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual bool OnSceneQuery(LayerMask[] prioritizedLayerMasks, bool focusIndividualCompoundCollider, out GameObject hitObject, out Vector3 hitPoint, out float hitDistance)
|
|
{
|
|
MixedRealityRaycastHit hitInfo = new MixedRealityRaycastHit();
|
|
bool querySuccessful = OnSceneQuery(prioritizedLayerMasks, focusIndividualCompoundCollider, out hitInfo, out _, out _);
|
|
|
|
hitObject = focusIndividualCompoundCollider ? hitInfo.collider.gameObject : hitInfo.transform.gameObject;
|
|
hitPoint = hitInfo.point;
|
|
hitDistance = hitInfo.distance;
|
|
|
|
return querySuccessful;
|
|
}
|
|
|
|
private static readonly ProfilerMarker OnPostSceneQueryPerfMarker = new ProfilerMarker("[MRTK] BaseControllerPointer.OnPostSceneQuery");
|
|
|
|
/// <inheritdoc />
|
|
public virtual void OnPostSceneQuery()
|
|
{
|
|
using (OnPostSceneQueryPerfMarker.Auto())
|
|
{
|
|
if (grabAction != MixedRealityInputAction.None && InputSourceParent.SourceType == InputSourceType.Controller)
|
|
{
|
|
if (IsGrabPressed)
|
|
{
|
|
CoreServices.InputSystem.RaisePointerDragged(this, grabAction, Handedness);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (IsSelectPressed)
|
|
{
|
|
CoreServices.InputSystem.RaisePointerDragged(this, MixedRealityInputAction.None, Handedness);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual void OnPreCurrentPointerTargetChange() { }
|
|
|
|
/// <inheritdoc />
|
|
public virtual void Reset()
|
|
{
|
|
Controller = null;
|
|
IsActive = false;
|
|
IsFocusLocked = false;
|
|
}
|
|
|
|
#endregion IMixedRealityPointer Implementation
|
|
|
|
#region IEquality Implementation
|
|
|
|
private static bool Equals(IMixedRealityPointer left, IMixedRealityPointer right)
|
|
{
|
|
return left.Equals(right);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
bool IEqualityComparer.Equals(object left, object right)
|
|
{
|
|
return left != null && left.Equals(right);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool Equals(object obj)
|
|
{
|
|
if (ReferenceEquals(null, obj)) { return false; }
|
|
if (ReferenceEquals(this, obj)) { return true; }
|
|
if (obj.GetType() != GetType()) { return false; }
|
|
|
|
return Equals((IMixedRealityPointer)obj);
|
|
}
|
|
|
|
private bool Equals(IMixedRealityPointer other)
|
|
{
|
|
return other != null && PointerId == other.PointerId && string.Equals(PointerName, other.PointerName);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
int IEqualityComparer.GetHashCode(object obj)
|
|
{
|
|
return obj.GetHashCode();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override int GetHashCode()
|
|
{
|
|
unchecked
|
|
{
|
|
int hashCode = 0;
|
|
hashCode = (hashCode * 397) ^ (int)PointerId;
|
|
hashCode = (hashCode * 397) ^ (PointerName != null ? PointerName.GetHashCode() : 0);
|
|
return hashCode;
|
|
}
|
|
}
|
|
|
|
#endregion IEquality Implementation
|
|
|
|
#region IMixedRealitySourcePoseHandler Implementation
|
|
|
|
private static readonly ProfilerMarker OnSourceLostPerfMarker = new ProfilerMarker("[MRTK] BaseControllerPointer.OnSourceLost");
|
|
|
|
/// <inheritdoc />
|
|
public override void OnSourceLost(SourceStateEventData eventData)
|
|
{
|
|
using (OnSourceLostPerfMarker.Auto())
|
|
{
|
|
base.OnSourceLost(eventData);
|
|
|
|
if (eventData.SourceId == InputSourceParent.SourceId)
|
|
{
|
|
if (requiresHoldAction)
|
|
{
|
|
IsHoldPressed = false;
|
|
}
|
|
|
|
if (IsSelectPressed)
|
|
{
|
|
CoreServices.InputSystem.RaisePointerUp(this, pointerAction, Handedness);
|
|
}
|
|
|
|
if (IsGrabPressed)
|
|
{
|
|
CoreServices.InputSystem.RaisePointerUp(this, grabAction, Handedness);
|
|
}
|
|
|
|
IsSelectPressed = false;
|
|
IsGrabPressed = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion IMixedRealitySourcePoseHandler Implementation
|
|
|
|
#region IMixedRealityInputHandler Implementation
|
|
|
|
private static readonly ProfilerMarker OnInputUpPerfMarker = new ProfilerMarker("[MRTK] BaseControllerPointer.OnInputUp");
|
|
|
|
/// <inheritdoc />
|
|
public override void OnInputUp(InputEventData eventData)
|
|
{
|
|
if (!IsInteractionEnabled) { return; }
|
|
|
|
using (OnInputUpPerfMarker.Auto())
|
|
{
|
|
base.OnInputUp(eventData);
|
|
|
|
if (eventData.SourceId == InputSourceParent.SourceId)
|
|
{
|
|
if (requiresHoldAction && eventData.MixedRealityInputAction == activeHoldAction)
|
|
{
|
|
IsHoldPressed = false;
|
|
}
|
|
|
|
if (grabAction != MixedRealityInputAction.None &&
|
|
eventData.InputSource.SourceType == InputSourceType.Controller &&
|
|
eventData.MixedRealityInputAction == grabAction)
|
|
{
|
|
IsGrabPressed = false;
|
|
|
|
CoreServices.InputSystem.RaisePointerClicked(this, grabAction, 0, Handedness);
|
|
CoreServices.InputSystem.RaisePointerUp(this, grabAction, Handedness);
|
|
}
|
|
|
|
if (eventData.MixedRealityInputAction == pointerAction)
|
|
{
|
|
IsSelectPressed = false;
|
|
|
|
CoreServices.InputSystem.RaisePointerClicked(this, pointerAction, 0, Handedness);
|
|
CoreServices.InputSystem.RaisePointerUp(this, pointerAction, Handedness);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static readonly ProfilerMarker OnInputDownPerfMarker = new ProfilerMarker("[MRTK] BaseControllerPointer.OnInputDown");
|
|
|
|
/// <inheritdoc />
|
|
public override void OnInputDown(InputEventData eventData)
|
|
{
|
|
if (!IsInteractionEnabled) { return; }
|
|
|
|
using (OnInputDownPerfMarker.Auto())
|
|
{
|
|
base.OnInputDown(eventData);
|
|
|
|
if (eventData.SourceId == InputSourceParent.SourceId)
|
|
{
|
|
if (requiresHoldAction && eventData.MixedRealityInputAction == activeHoldAction)
|
|
{
|
|
IsHoldPressed = true;
|
|
}
|
|
|
|
if (grabAction != MixedRealityInputAction.None &&
|
|
eventData.InputSource.SourceType == InputSourceType.Controller &&
|
|
eventData.MixedRealityInputAction == grabAction)
|
|
{
|
|
IsGrabPressed = true;
|
|
|
|
if (IsInteractionEnabled)
|
|
{
|
|
CoreServices.InputSystem.RaisePointerDown(this, grabAction, Handedness);
|
|
}
|
|
}
|
|
|
|
if (eventData.MixedRealityInputAction == pointerAction)
|
|
{
|
|
IsSelectPressed = true;
|
|
HasSelectPressedOnce = true;
|
|
|
|
if (IsInteractionEnabled)
|
|
{
|
|
CoreServices.InputSystem.RaisePointerDown(this, pointerAction, Handedness);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion IMixedRealityInputHandler Implementation
|
|
}
|
|
}
|