/******************************************************************************
* Copyright (C) Ultraleap, Inc. 2011-2021. *
* *
* Use subject to the terms of the Apache License 2.0 available at *
* http://www.apache.org/licenses/LICENSE-2.0, or another agreement *
* between Ultraleap and you, your company or other organization. *
******************************************************************************/
using Leap.Unity.Attributes;
using Leap.Unity.Query;
using System;
using System.Collections;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
namespace Leap.Unity.Attachments
{
///
/// This MonoBehaviour is managed by an AttachmentHands component on a parent MonoBehaviour.
/// Instead of adding AttachmentHand directly to a GameObject, add an AttachmentHands component
/// to a parent GameObject to manage the construction and updating of AttachmentHand objects.
///
[AddComponentMenu("")]
[ExecuteInEditMode]
public class AttachmentHand : MonoBehaviour
{
///
/// Called when the AttachmentHand refreshes its AttachmentPointBehaviour transforms. If the
/// user unchecks an attachment point in the AttachmentHands inspector, those Transforms will
/// be destroyed; otherwise, existing Transforms will persist, so be careful not to unnecessarily
/// duplicate any objects or components you may want to attach via this callback.
///
/// Also, you can use AttachmentHand.points for an enumerator of all existing AttachmentPointBehaviour
/// transforms on a given AttachmentHand object.
///
public Action OnAttachmentPointsModified = () => { };
#region AttachmentPointBehaviours
[HideInInspector]
public AttachmentPointBehaviour wrist;
[HideInInspector]
public AttachmentPointBehaviour palm;
[HideInInspector]
public AttachmentPointBehaviour thumbProximalJoint;
[HideInInspector]
public AttachmentPointBehaviour thumbDistalJoint;
[HideInInspector]
public AttachmentPointBehaviour thumbTip;
[HideInInspector]
public AttachmentPointBehaviour indexKnuckle;
[HideInInspector]
public AttachmentPointBehaviour indexMiddleJoint;
[HideInInspector]
public AttachmentPointBehaviour indexDistalJoint;
[HideInInspector]
public AttachmentPointBehaviour indexTip;
[HideInInspector]
public AttachmentPointBehaviour middleKnuckle;
[HideInInspector]
public AttachmentPointBehaviour middleMiddleJoint;
[HideInInspector]
public AttachmentPointBehaviour middleDistalJoint;
[HideInInspector]
public AttachmentPointBehaviour middleTip;
[HideInInspector]
public AttachmentPointBehaviour ringKnuckle;
[HideInInspector]
public AttachmentPointBehaviour ringMiddleJoint;
[HideInInspector]
public AttachmentPointBehaviour ringDistalJoint;
[HideInInspector]
public AttachmentPointBehaviour ringTip;
[HideInInspector]
public AttachmentPointBehaviour pinkyKnuckle;
[HideInInspector]
public AttachmentPointBehaviour pinkyMiddleJoint;
[HideInInspector]
public AttachmentPointBehaviour pinkyDistalJoint;
[HideInInspector]
public AttachmentPointBehaviour pinkyTip;
#endregion
///
/// Gets an enumerator that traverses all of the AttachmentPoints beneath this AttachmentHand.
///
public AttachmentPointsEnumerator points { get { return new AttachmentPointsEnumerator(this); } }
private bool _attachmentPointsDirty = false;
///
/// Used by AttachmentHands as a hint to help prevent mixing up hand chiralities
/// when refreshing its AttachmentHand references.
///
[SerializeField, Disable]
private Chirality _chirality;
///
/// Gets the chirality of this AttachmentHand. This is set automatically by the
/// AttachmentHands parent object of this AttachmentHand.
///
public Chirality chirality { get { return _chirality; } set { _chirality = value; } }
///
/// Used by AttachmentHands as a hint to help prevent mixing up hand chiralities
/// when refreshing its AttachmentHand references.
///
[SerializeField, Disable]
private bool _isTracked;
///
/// Gets the chirality of this AttachmentHand. This is set automatically by the
/// AttachmentHands parent object of this AttachmentHand.
///
public bool isTracked { get { return _isTracked; } set { _isTracked = value; } }
void OnValidate()
{
initializeAttachmentPointFlagConstants();
}
void Awake()
{
initializeAttachmentPointFlagConstants();
}
#if !UNITY_EDITOR
#pragma warning disable 0414
#endif
private bool _isBeingDestroyed = false;
#if !UNITY_EDITOR
#pragma warning restore 0414
#endif
void OnDestroy()
{
_isBeingDestroyed = true;
}
///
/// Returns the AttachmentPointBehaviour child object of this AttachmentHand given a
/// reference to a single AttachmentPointFlags flag, or null if there is no such child object.
///
public AttachmentPointBehaviour GetBehaviourForPoint(AttachmentPointFlags singlePoint)
{
AttachmentPointBehaviour behaviour = null;
switch (singlePoint)
{
case AttachmentPointFlags.None: break;
case AttachmentPointFlags.Wrist: behaviour = wrist; break;
case AttachmentPointFlags.Palm: behaviour = palm; break;
case AttachmentPointFlags.ThumbProximalJoint: behaviour = thumbProximalJoint; break;
case AttachmentPointFlags.ThumbDistalJoint: behaviour = thumbDistalJoint; break;
case AttachmentPointFlags.ThumbTip: behaviour = thumbTip; break;
case AttachmentPointFlags.IndexKnuckle: behaviour = indexKnuckle; break;
case AttachmentPointFlags.IndexMiddleJoint: behaviour = indexMiddleJoint; break;
case AttachmentPointFlags.IndexDistalJoint: behaviour = indexDistalJoint; break;
case AttachmentPointFlags.IndexTip: behaviour = indexTip; break;
case AttachmentPointFlags.MiddleKnuckle: behaviour = middleKnuckle; break;
case AttachmentPointFlags.MiddleMiddleJoint: behaviour = middleMiddleJoint; break;
case AttachmentPointFlags.MiddleDistalJoint: behaviour = middleDistalJoint; break;
case AttachmentPointFlags.MiddleTip: behaviour = middleTip; break;
case AttachmentPointFlags.RingKnuckle: behaviour = ringKnuckle; break;
case AttachmentPointFlags.RingMiddleJoint: behaviour = ringMiddleJoint; break;
case AttachmentPointFlags.RingDistalJoint: behaviour = ringDistalJoint; break;
case AttachmentPointFlags.RingTip: behaviour = ringTip; break;
case AttachmentPointFlags.PinkyKnuckle: behaviour = pinkyKnuckle; break;
case AttachmentPointFlags.PinkyMiddleJoint: behaviour = pinkyMiddleJoint; break;
case AttachmentPointFlags.PinkyDistalJoint: behaviour = pinkyDistalJoint; break;
case AttachmentPointFlags.PinkyTip: behaviour = pinkyTip; break;
}
return behaviour;
}
public void refreshAttachmentTransforms(AttachmentPointFlags points)
{
if (_attachmentPointFlagConstants == null || _attachmentPointFlagConstants.Length == 0)
{
initializeAttachmentPointFlagConstants();
}
// First just _check_ whether we'll need to do any destruction or creation
bool requiresDestructionOrCreation = false;
foreach (var flag in _attachmentPointFlagConstants)
{
if (flag == AttachmentPointFlags.None) continue;
if ((!points.Contains(flag) && GetBehaviourForPoint(flag) != null)
|| (points.Contains(flag) && GetBehaviourForPoint(flag) == null))
{
requiresDestructionOrCreation = true;
}
}
// Go through the work of flattening and rebuilding if it is necessary.
if (requiresDestructionOrCreation)
{
// Remove parent-child relationships so deleting parent Transforms doesn't annihilate
// child Transforms that don't need to be deleted themselves.
flattenAttachmentTransformHierarchy();
foreach (AttachmentPointFlags flag in _attachmentPointFlagConstants)
{
if (flag == AttachmentPointFlags.None) continue;
if (points.Contains(flag))
{
ensureTransformExists(flag);
}
else
{
ensureTransformDoesNotExist(flag);
}
}
// Organize transforms, restoring parent-child relationships.
organizeAttachmentTransforms();
}
if (_attachmentPointsDirty)
{
OnAttachmentPointsModified();
_attachmentPointsDirty = false;
}
}
public void notifyPointBehaviourDeleted(AttachmentPointBehaviour point)
{
#if UNITY_EDITOR
// Only valid if the AttachmentHand itself is also not being destroyed.
if (_isBeingDestroyed) return;
// Refresh this hand's attachment transforms on a slight delay.
// Only AttachmentHands can _truly_ remove attachment points!
AttachmentHands attachHands = GetComponentInParent();
if (attachHands != null)
{
EditorApplication.delayCall += () => { refreshAttachmentTransforms(attachHands.attachmentPoints); };
}
#endif
}
#region Internal
private AttachmentPointFlags[] _attachmentPointFlagConstants;
private void initializeAttachmentPointFlagConstants()
{
Array flagConstants = Enum.GetValues(typeof(AttachmentPointFlags));
if (_attachmentPointFlagConstants == null || _attachmentPointFlagConstants.Length == 0)
{
_attachmentPointFlagConstants = new AttachmentPointFlags[flagConstants.Length];
}
int i = 0;
foreach (int f in flagConstants)
{
_attachmentPointFlagConstants[i++] = (AttachmentPointFlags)f;
}
}
private void setBehaviourForPoint(AttachmentPointFlags singlePoint, AttachmentPointBehaviour behaviour)
{
switch (singlePoint)
{
case AttachmentPointFlags.None: break;
case AttachmentPointFlags.Wrist: wrist = behaviour; break;
case AttachmentPointFlags.Palm: palm = behaviour; break;
case AttachmentPointFlags.ThumbProximalJoint: thumbProximalJoint = behaviour; break;
case AttachmentPointFlags.ThumbDistalJoint: thumbDistalJoint = behaviour; break;
case AttachmentPointFlags.ThumbTip: thumbTip = behaviour; break;
case AttachmentPointFlags.IndexKnuckle: indexKnuckle = behaviour; break;
case AttachmentPointFlags.IndexMiddleJoint: indexMiddleJoint = behaviour; break;
case AttachmentPointFlags.IndexDistalJoint: indexDistalJoint = behaviour; break;
case AttachmentPointFlags.IndexTip: indexTip = behaviour; break;
case AttachmentPointFlags.MiddleKnuckle: middleKnuckle = behaviour; break;
case AttachmentPointFlags.MiddleMiddleJoint: middleMiddleJoint = behaviour; break;
case AttachmentPointFlags.MiddleDistalJoint: middleDistalJoint = behaviour; break;
case AttachmentPointFlags.MiddleTip: middleTip = behaviour; break;
case AttachmentPointFlags.RingKnuckle: ringKnuckle = behaviour; break;
case AttachmentPointFlags.RingMiddleJoint: ringMiddleJoint = behaviour; break;
case AttachmentPointFlags.RingDistalJoint: ringDistalJoint = behaviour; break;
case AttachmentPointFlags.RingTip: ringTip = behaviour; break;
case AttachmentPointFlags.PinkyKnuckle: pinkyKnuckle = behaviour; break;
case AttachmentPointFlags.PinkyMiddleJoint: pinkyMiddleJoint = behaviour; break;
case AttachmentPointFlags.PinkyDistalJoint: pinkyDistalJoint = behaviour; break;
case AttachmentPointFlags.PinkyTip: pinkyTip = behaviour; break;
}
#if UNITY_EDITOR
EditorUtility.SetDirty(this);
#endif
}
private void ensureTransformExists(AttachmentPointFlags singlePoint)
{
if (!singlePoint.IsSinglePoint())
{
Debug.LogError("Tried to ensure transform exists for singlePoint, but it contains more than one set flag.");
return;
}
AttachmentPointBehaviour pointBehaviour = GetBehaviourForPoint(singlePoint);
if (pointBehaviour == null)
{
// First, see if there's already one in the hierarchy! Might exist due to, e.g. an Undo operation
var existingPointBehaviour = this.gameObject.GetComponentsInChildren()
.Query()
.FirstOrDefault(p => p.attachmentPoint == singlePoint);
// Only make a new object if the transform really doesn't exist.
if (existingPointBehaviour == AttachmentPointFlags.None)
{
GameObject obj = new GameObject(Enum.GetName(typeof(AttachmentPointFlags), singlePoint));
#if UNITY_EDITOR
Undo.RegisterCreatedObjectUndo(obj, "Created Object");
pointBehaviour = Undo.AddComponent(obj);
#else
pointBehaviour = obj.AddComponent();
#endif
}
else
{
pointBehaviour = existingPointBehaviour;
}
#if UNITY_EDITOR
Undo.RecordObject(pointBehaviour, "Set Attachment Point");
#endif
pointBehaviour.attachmentPoint = singlePoint;
pointBehaviour.attachmentHand = this;
setBehaviourForPoint(singlePoint, pointBehaviour);
SetTransformParent(pointBehaviour.transform, this.transform);
_attachmentPointsDirty = true;
#if UNITY_EDITOR
EditorUtility.SetDirty(this);
#endif
}
}
private static void SetTransformParent(Transform t, Transform parent)
{
#if UNITY_EDITOR
Undo.SetTransformParent(t, parent, "Set Transform Parent");
#else
t.parent = parent;
#endif
}
private void ensureTransformDoesNotExist(AttachmentPointFlags singlePoint)
{
if (!singlePoint.IsSinglePoint())
{
Debug.LogError("Tried to ensure transform exists for singlePoint, but it contains more than one set flag");
return;
}
var pointBehaviour = GetBehaviourForPoint(singlePoint);
if (pointBehaviour != null)
{
InternalUtility.Destroy(pointBehaviour.gameObject);
setBehaviourForPoint(singlePoint, null);
pointBehaviour = null;
_attachmentPointsDirty = true;
#if UNITY_EDITOR
EditorUtility.SetDirty(this);
#endif
}
}
private void flattenAttachmentTransformHierarchy()
{
foreach (var point in this.points)
{
SetTransformParent(point.transform, this.transform);
}
}
private void organizeAttachmentTransforms()
{
int siblingIdx = 0;
// Wrist
if (wrist != null)
{
wrist.transform.SetSiblingIndex(siblingIdx++);
}
// Palm
if (palm != null)
{
palm.transform.SetSiblingIndex(siblingIdx++);
}
Transform topLevelTransform;
// Thumb
topLevelTransform = tryStackTransformHierarchy(thumbProximalJoint,
thumbDistalJoint,
thumbTip);
if (topLevelTransform != null)
{
topLevelTransform.SetSiblingIndex(siblingIdx++);
}
// Index
topLevelTransform = tryStackTransformHierarchy(indexKnuckle,
indexMiddleJoint,
indexDistalJoint,
indexTip);
if (topLevelTransform != null)
{
topLevelTransform.SetSiblingIndex(siblingIdx++);
}
// Middle
topLevelTransform = tryStackTransformHierarchy(middleKnuckle,
middleMiddleJoint,
middleDistalJoint,
middleTip);
if (topLevelTransform != null)
{
topLevelTransform.SetSiblingIndex(siblingIdx++);
}
// Ring
topLevelTransform = tryStackTransformHierarchy(ringKnuckle,
ringMiddleJoint,
ringDistalJoint,
ringTip);
if (topLevelTransform != null)
{
topLevelTransform.SetSiblingIndex(siblingIdx++);
}
// Pinky
topLevelTransform = tryStackTransformHierarchy(pinkyKnuckle,
pinkyMiddleJoint,
pinkyDistalJoint,
pinkyTip);
if (topLevelTransform != null)
{
topLevelTransform.SetSiblingIndex(siblingIdx++);
}
}
private static Transform[] s_hierarchyTransformsBuffer = new Transform[4];
///
/// Tries to build a parent-child stack (index 0 is the first parent) of the argument
/// transforms (they might be null) and returns the top-level parent transform (or null
/// if there is none).
///
private Transform tryStackTransformHierarchy(params Transform[] transforms)
{
for (int i = 0; i < s_hierarchyTransformsBuffer.Length; i++)
{
s_hierarchyTransformsBuffer[i] = null;
}
int hierarchyCount = 0;
foreach (var transform in transforms.Query().Where(t => t != null))
{
s_hierarchyTransformsBuffer[hierarchyCount++] = transform;
}
for (int i = hierarchyCount - 1; i > 0; i--)
{
SetTransformParent(s_hierarchyTransformsBuffer[i], s_hierarchyTransformsBuffer[i - 1]);
}
if (hierarchyCount > 0)
{
return s_hierarchyTransformsBuffer[0];
}
return null;
}
private static Transform[] s_transformsBuffer = new Transform[4];
private Transform tryStackTransformHierarchy(params MonoBehaviour[] monoBehaviours)
{
for (int i = 0; i < s_transformsBuffer.Length; i++)
{
s_transformsBuffer[i] = null;
}
int tIdx = 0;
foreach (var behaviour in monoBehaviours.Query().Where(b => b != null))
{
s_transformsBuffer[tIdx++] = behaviour.transform;
}
return tryStackTransformHierarchy(s_transformsBuffer);
}
///
/// An enumerator that traverses all of the existing AttachmentPointBehaviours beneath an
/// AttachmentHand.
///
public struct AttachmentPointsEnumerator
{
private int _curIdx;
private AttachmentHand _hand;
private int _flagsCount;
public AttachmentPointsEnumerator GetEnumerator() { return this; }
public AttachmentPointsEnumerator(AttachmentHand hand)
{
if (hand != null && hand._attachmentPointFlagConstants != null)
{
_curIdx = -1;
_hand = hand;
_flagsCount = hand._attachmentPointFlagConstants.Length;
}
else
{
// Hand doesn't exist (destroyed?) or isn't initialized yet.
_curIdx = -1;
_hand = null;
_flagsCount = 0;
}
}
public AttachmentPointBehaviour Current
{
get
{
if (_hand == null) return null;
return _hand.GetBehaviourForPoint(GetFlagFromFlagIdx(_curIdx));
}
}
public bool MoveNext()
{
do
{
_curIdx++;
} while (_curIdx < _flagsCount && _hand.GetBehaviourForPoint(GetFlagFromFlagIdx(_curIdx)) == null);
return _curIdx < _flagsCount;
}
}
private static AttachmentPointFlags GetFlagFromFlagIdx(int pointIdx)
{
return (AttachmentPointFlags)(1 << pointIdx + 1);
}
#endregion
}
}