using System.Collections.Generic;
/// Re-targets a tracker/controller (“<see cref="target"/>”)
/// to a VRIK hand/foot/etc. target, with optional
/// * X-axis mirroring (left↔right),
/// * Y-axis mirroring (up↔down / crouch↔stand),
/// * fixed delay buffering,
/// * selective position / rotation / ground locking.
/// All reflections are performed **in the coordinate
/// system of <see cref="root"/>**, so the mirror plane stays
/// glued to the avatar no matter where it moves.
[DisallowMultipleComponent]
public sealed class HandRetargeting : MonoBehaviour
/* --------------------------------------------------------------------
* ------------------------------------------------------------------*/
[Tooltip("The physical tracker or XR controller you want to copy.")]
[Tooltip("Coordinate frame used for mirroring & remapping. "
+ "Usually the avatar’s hips/root transform.")]
/* --------------------------------------------------------------------
* Inspector – Behaviour toggles
* ------------------------------------------------------------------*/
public bool ignorePosition;
public bool ignoreRotation;
public bool lockToGround; // y → 0 in root-local space
/* --------------------------------------------------------------------
* Inspector – Axis mirroring
* ------------------------------------------------------------------*/
[Tooltip("Mirror left↔right (local X)")]
[Tooltip("Mirror up↔down (local Y)")]
/* --------------------------------------------------------------------
* Inspector – Optional (non-linear) remapping via BoxColliders
* ------------------------------------------------------------------*/
[Header("Remapping (optional)")]
public BoxCollider inputBounds;
public BoxCollider outputBounds;
/* --------------------------------------------------------------------
* ------------------------------------------------------------------*/
[Header("Fixed delay (seconds)")]
public float delayTime = 0.5f;
/* --------------------------------------------------------------------
* ------------------------------------------------------------------*/
readonly Queue<Vector3> _posQueue = new();
readonly Queue<Quaternion> _rotQueue = new();
float _lastFixedDelta = -1f;
/* ====================================================================
* ===================================================================*/
RecalculateDelayFrames();
// Re-calculate delay if project changes Time.fixedDeltaTime on the fly
if (!Mathf.Approximately(Time.fixedDeltaTime, _lastFixedDelta))
RecalculateDelayFrames();
/* ------------------------------------------------------------
Vector3 worldPos = target.position;
Quaternion worldRot = target.rotation;
/* ------------------------------------------------------------
* (2) Convert to root-local space */
Vector3 localPos = root.InverseTransformPoint(worldPos);
Quaternion localRot = Quaternion.Inverse(root.rotation) * worldRot;
/* ------------------------------------------------------------
if (useRemap && inputBounds && outputBounds)
localPos = Remap(localPos);
/* ------------------------------------------------------------
localPos.x = -localPos.x;
localRot = new Quaternion(-localRot.x, localRot.y,
localRot.z, -localRot.w);
localPos.y = -localPos.y;
localRot = new Quaternion( localRot.x, -localRot.y,
localRot.z, -localRot.w);
/* ------------------------------------------------------------
* (5) Lock / ignore flags */
Vector3 finalLocalPos = ignorePosition ? transform.localPosition : localPos;
Quaternion finalLocalRot = ignoreRotation ? transform.localRotation : localRot;
/* ------------------------------------------------------------
* (6) Back to world space */
Vector3 finalWorldPos = root.TransformPoint(finalLocalPos);
Quaternion finalWorldRot = root.rotation * finalLocalRot;
/* ------------------------------------------------------------
// If no delay requested, write straight away.
Apply(finalWorldPos, finalWorldRot);
_posQueue.Enqueue(finalWorldPos);
_rotQueue.Enqueue(finalWorldRot);
if (_posQueue.Count > _delayFrames)
Apply(_posQueue.Dequeue(), _rotQueue.Dequeue());
/* ====================================================================
* ===================================================================*/
void RecalculateDelayFrames()
_lastFixedDelta = Time.fixedDeltaTime;
_delayFrames = Mathf.RoundToInt(delayTime / _lastFixedDelta);
void Apply(Vector3 worldPos, Quaternion worldRot)
transform.position = worldPos;
transform.rotation = worldRot;
/// Maps a point from <see cref="inputBounds"/> to <see cref="outputBounds"/>
/// in each axis independently (linear interpolation).
Vector3 Remap(Vector3 localPos)
Vector3 inMin = BoundsLocalMin(inputBounds);
Vector3 inMax = BoundsLocalMax(inputBounds);
Vector3 outMin = BoundsLocalMin(outputBounds);
Vector3 outMax = BoundsLocalMax(outputBounds);
// Normalise to [0,1] inside input bounds
t.x = Mathf.InverseLerp(inMin.x, inMax.x, localPos.x);
t.y = Mathf.InverseLerp(inMin.y, inMax.y, localPos.y);
t.z = Mathf.InverseLerp(inMin.z, inMax.z, localPos.z);
// Re-scale to output bounds
localPos.x = Mathf.Lerp(outMin.x, outMax.x, t.x);
localPos.y = Mathf.Lerp(outMin.y, outMax.y, t.y);
localPos.z = Mathf.Lerp(outMin.z, outMax.z, t.z);
static Vector3 BoundsLocalMin(BoxCollider col)
=> col.transform.InverseTransformPoint(col.bounds.min);
static Vector3 BoundsLocalMax(BoxCollider col)
=> col.transform.InverseTransformPoint(col.bounds.max);