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