/****************************************************************************** * 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 } }