// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- using System; using System.Collections.Generic; using UltimateXR.Core; using UltimateXR.Core.Math; using UltimateXR.Extensions.Unity.Render; using UnityEngine; namespace UltimateXR.Editor.Manipulation.HandPoses { /// /// Finger spinner widget that allows to handle the rotation of finger nodes. It is placed inside a hand image together /// with all other finger spinners. /// [Serializable] public class UxrFingerSpinner { #region Inspector Properties/Serialized Fields [SerializeField] private UxrFingerAngleType _angleType; [SerializeField] private Texture2D _texMouse; [SerializeField] private Color32 _color; [SerializeField] private Transform _target; [SerializeField] private Transform _parent; [SerializeField] private UxrUniversalLocalAxes _targetLocalAxes; [SerializeField] private UxrUniversalLocalAxes _parentLocalAxes; [SerializeField] private float _minAngle; [SerializeField] private float _maxAngle; [SerializeField] private UxrHandSide _handSide; [SerializeField] private bool _isThumbProximal; [SerializeField] private Vector2 _quadMax; [SerializeField] private Vector2 _quadMin; [SerializeField] private float _offset; #endregion #region Public Types & Data /// /// Gets the type of angle controlled by the spinner. /// public UxrFingerAngleType Angle => _angleType; /// /// Gets the finger bone transform controlled by the spinner. /// public Transform Target => _target; /// /// Gets which hand the spinner belongs to. /// public UxrHandSide HandSide => _handSide; /// /// Gets the of the UI control in local hand image coordinates. /// public Rect MouseRect => new Rect(_quadMin.x, _quadMin.y, _quadMax.x - _quadMin.x, _quadMax.y - _quadMin.y); /// /// Gets or sets the angle value in degrees. /// public float Value { get => GetValueFromObject(); set { float oldValue = GetValueFromObject(); float newValue = Mathf.Clamp(value, _minAngle, _maxAngle); if (_angleType == UxrFingerAngleType.Spread) { _target.Rotate(_targetLocalAxes.LocalUp, newValue - oldValue, Space.Self); } else if (_angleType == UxrFingerAngleType.Curl) { _target.Rotate(_targetLocalAxes.LocalRight, newValue - oldValue, Space.Self); } } } /// /// Gets or sets the offset applied to the spinner. It is used to draw the widget instead of using /// which sometimes propagates slightly from one spinner to another since transforms are related. /// public float Offset { get => _offset; set { float forgiveness = 0.1f; if (Value > _minAngle + forgiveness && Value < _maxAngle - forgiveness) { _offset = value; } } } #endregion #region Constructors & Finalizer /// /// Constructor. /// /// The type of angle this control will rotate (spread or curl) /// The image that has the clickable parts encoded in colors /// The color this control has inside the texture /// The transform that will be rotated /// The parent transform /// The universal right/up/forward axes of the target transform /// The universal right/up/forward axes of the parent transform /// The minimum allowed value /// The maximum allowed angle /// Which hand /// Is target a proximal bone from the thumb? public UxrFingerSpinner(UxrFingerAngleType angleType, Texture2D texMouse, Color32 color, Transform target, Transform parent, UxrUniversalLocalAxes targetLocalAxes, UxrUniversalLocalAxes parentLocalAxes, float minAngle, float maxAngle, UxrHandSide handSide, bool isThumbProximal = false) { _angleType = angleType; _texMouse = texMouse; _color = color; _target = target; _parent = parent; _targetLocalAxes = targetLocalAxes; _parentLocalAxes = parentLocalAxes; _minAngle = minAngle; _maxAngle = maxAngle; _handSide = handSide; _isThumbProximal = isThumbProximal; if (handSide == UxrHandSide.Left && s_boundsLeftHand == null) { s_boundsLeftHand = GetMouseTextureBounds(texMouse); } if (handSide == UxrHandSide.Right && s_boundsRightHand == null) { s_boundsRightHand = GetMouseTextureBounds(texMouse); } _quadMin = GetBoundsMin(handSide == UxrHandSide.Left ? s_boundsLeftHand : s_boundsRightHand, color); _quadMax = GetBoundsMax(handSide == UxrHandSide.Left ? s_boundsLeftHand : s_boundsRightHand, color); } #endregion #region Public Methods /// /// Checks whether the mouse position lies within the control bounds. /// /// Mouse position in local hand image coordinates /// Whether the mouse position lies within the control bounds public bool ContainsMousePos(Vector2 mousePos) { return mousePos.x >= _quadMin.x && mousePos.x <= _quadMax.x && mousePos.y >= _quadMin.y && mousePos.y <= _quadMax.y; /* * Alternative: Get from mouse texture. * if (mousePos.x >= 0 && mousePos.x < _texMouse.width && mousePos.y >= 0 && mousePos.y < _texMouse.height) { Color32 texColor = _texMouse.GetPixels32(0)[((_texMouse.height - Mathf.RoundToInt(mousePos.y)) * _texMouse.width) + Mathf.RoundToInt(mousePos.x)]; return texColor.r == _color.r && texColor.g == _color.g && texColor.b == _color.b && texColor.a == _color.a; } return false; */ } /// /// Computes the rotation angle from the object's current transform. /// /// Rotation value in degrees public float GetValueFromObject() { return GetValueFromObject(_angleType); } #endregion #region Private Methods /// /// Computes a dictionary where for a given color a minimum and maximum rect value is stored, representing the corners /// where the color appears in the texture. /// /// Texture /// Dictionary private static Dictionary GetMouseTextureBounds(Texture2D texture) { Dictionary colorBounds = new Dictionary(); Color32[] pixels = texture.GetPixels32(0); for (int x = 0; x < texture.width; ++x) { for (int y = 0; y < texture.height; ++y) { int color = pixels[(texture.height - y - 1) * texture.width + x].ToInt(); if (!colorBounds.ContainsKey(color)) { colorBounds.Add(color, new Bounds(new Vector3(x, y, 0.0f), Vector3.zero)); } Bounds bounds = colorBounds[color]; Vector3 min = bounds.min; Vector3 max = bounds.max; if (x < min.x) { min.x = x; } if (x > max.x) { max.x = x; } if (y < min.y) { min.y = y; } if (y > max.y) { max.y = y; } bounds.min = min; bounds.max = max; colorBounds[color] = bounds; } } return colorBounds; } /// /// Gets the minimum value of a rect for a given color. /// /// Bounds dictionary /// Color to get the max bounds value of /// Maximum rect x and y for the given color private static Vector2 GetBoundsMin(Dictionary colorBounds, Color32 color) { if (colorBounds.TryGetValue(color.ToInt(), out Bounds bounds)) { return new Vector2(bounds.min.x, bounds.min.y); } return Vector2.zero; } /// /// Gets the maximum value of a rect for a given color. /// /// Bounds dictionary /// Color to get the max bounds value of /// Maximum rect x and y for the given color private static Vector2 GetBoundsMax(Dictionary colorBounds, Color32 color) { if (colorBounds.TryGetValue(color.ToInt(), out Bounds bounds)) { return new Vector2(bounds.max.x, bounds.max.y); } return Vector2.zero; } /// /// Computes a rotation angle from the object's current transform. /// /// Angle to compute (spread or curl) /// Angle in degrees private float GetValueFromObject(UxrFingerAngleType angleType) { // Compute roll value by rotating "right" vector from the current forward direction to the initial forward direction. Roll value will be // the angle between initial right and current right. // The parent reference directions are different for the proximal thumb bone which is a special case. Vector3 parentRight = _isThumbProximal ? _handSide == UxrHandSide.Left ? _parent.TransformDirection(-_parentLocalAxes.LocalForward) : _parent.TransformDirection(_parentLocalAxes.LocalForward) : _parent.TransformDirection(_parentLocalAxes.LocalRight); Vector3 parentUp = _parent.TransformDirection(_parentLocalAxes.LocalUp); Vector3 parentForward = _isThumbProximal ? _handSide == UxrHandSide.Left ? _parent.TransformDirection(_parentLocalAxes.LocalRight) : _parent.TransformDirection(-_parentLocalAxes.LocalRight) : _parent.TransformDirection(_parentLocalAxes.LocalForward); Vector3 targetRight = _target.TransformDirection(_targetLocalAxes.LocalRight); Vector3 targetUp = _target.TransformDirection(_targetLocalAxes.LocalUp); Vector3 targetForward = _target.TransformDirection(_targetLocalAxes.LocalForward); Quaternion toForward = Quaternion.FromToRotation(targetForward, parentForward); Vector3 rightOnlyRoll = toForward * targetRight; float roll = Vector3.SignedAngle(parentRight, rightOnlyRoll, parentForward); // Compute rotation to remove roll component Quaternion removeRoll = Quaternion.AngleAxis(-roll, targetForward); if (angleType == UxrFingerAngleType.Spread) { // Compute correct spread angle Vector3 spreadAxis = removeRoll * targetUp; Vector3 planeRightNormal = parentRight; Vector3 spreadVectorOnPlane = Vector3.Cross(planeRightNormal, Vector3.ProjectOnPlane(spreadAxis, planeRightNormal)); return Vector3.SignedAngle(spreadVectorOnPlane, targetForward, spreadAxis); } if (angleType == UxrFingerAngleType.Curl) { // Compute correct curl angle Vector3 curlAxis = removeRoll * targetRight; Vector3 planeUpNormal = parentUp; Vector3 curlVectorOnPlane = Vector3.Cross(Vector3.ProjectOnPlane(curlAxis, planeUpNormal), planeUpNormal); return Vector3.SignedAngle(curlVectorOnPlane, targetForward, curlAxis); } return 0.0f; } #endregion #region Private Types & Data private static Dictionary s_boundsLeftHand; private static Dictionary s_boundsRightHand; #endregion } }