Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
using Leap.Unity.Attributes;
using Leap.Unity.Geometry;
using Leap.Unity.Infix;
using Leap.Unity.Interaction;
using Leap.Unity.RuntimeGizmos;
using System;
using UnityEngine;
namespace Leap.Unity {
//using UnityRect = UnityEngine.Rect;
using Rect = Geometry.Rect;
public class HingeElement : MonoBehaviour, IRuntimeGizmoComponent {
#region Inspector
public InteractionBehaviour intObj;
[Header("Hinge Joint")]
[SerializeField]
private Transform _hingeTransform;
public Transform hingeTransform {
get {
if (_hingeTransform == null) return this.transform;
return _hingeTransform;
}
}
[SerializeField]
private LocalRect _localButtonSurface;
public Rect buttonSurface {
get {
return _localButtonSurface.With(this.transform);
}
}
public Vector2 minMaxHingeAngle = new Vector2(-30f, 80f);
[Header("Hinge Spring")]
[MinValue(0f)]
public float restSpringForce = 300f;
[MinValue(0f)]
public float restSpringFriction = 10f;
[MinValue(0f)]
public float restSpringDrag = 0.07f;
[Header("Debug")]
public bool drawDebug = false;
#endregion
#region State
public Plane currentPlane {
get {
return new Plane(-hingeTransform.forward, hingeTransform.position);
}
}
public Vector3 hingePosition {
get {
return hingeTransform.position;
}
}
private Vector3 _hingeLocalAxis = Vector3.up;
public Vector3 hingeAxis {
get {
return hingeTransform.rotation * _hingeLocalAxis;
}
}
private Vector3 _hingeLocalZeroAngleDir = Vector3.right;
public Vector3 hingeRestAngleDir {
get {
return hingeTransform.rotation * _hingeLocalZeroAngleDir;
}
}
private Vector3 _elementLocalActivationAxis = Vector3.forward;
/// <summary>
/// The (current) direction of activation ("pressing") of the hinging element,
/// somewhere on the plane about the hinge and orthogonal to the element direction.
/// </summary>
public Vector3 activationDir {
get {
return _hingeTransform.rotation
* Quaternion.AngleAxis(_elementAngle, _hingeLocalAxis)
* _elementLocalActivationAxis;
}
}
private Vector3 _elementLocalExtensionAxis = Vector3.right;
/// <summary>
/// The direction of the hinging element, somewhere on the plane about the hinge.
/// If the element were a flag, this is the direction the flag points. This direction
/// is up-to-date with the current _elementAngle value.
/// </summary>
public Vector3 elementDir {
get {
return _hingeTransform.rotation
* Quaternion.AngleAxis(_elementAngle, _hingeLocalAxis)
* _elementLocalExtensionAxis;
}
}
/// <summary>
/// The angle of the hinging element from the rest angle of the hinge.
///
/// At the beginning of each hinge Update, this angle is recalculated based on the
/// element transform and the hinge transform. The update step applies resting and
/// depenetration forces directly to this angle, then the angle is used to place the
/// element Transform relative to the hinge Transform.
/// </summary>
private float _elementAngle = 0f;
/// <summary>
/// The angle of the hinging element from the rest angle of the hinge. (Read only.)
/// </summary>
public float angle {
get {
return _elementAngle;
}
}
/// <summary>
/// The hinge-space resting pose of the element when the hinge is at its resting
/// angle.
///
/// Multiply the hinge transform by this pose to get the world-space resting pose of
/// the hinging element. This pose is calculated in Start.
/// </summary>
private Pose _elementRestPose_hinge;
private Vector3? _interactorPos = null;
/// <summary>
/// Gets whether this hinge element is currently being hovered over by an
/// interactor, such as a fingertip.
/// </summary>
public bool isHovered {
get {
return _interactorPos.HasValue;
}
}
/// <summary>
/// Public-facing state for other elements to know if this hinge element is currently
/// interacting with (touching) an interactor (e.g. fingertip).
/// </summary>
private bool _isInteracting = false;
public bool isInteracting {
get {
return _isInteracting;
}
}
private Vector3? _lastInteractorPos = null;
private float _interactorRadius = 0.0051f;
private float _restCorrectionVelocity = 0f;
// Debug data.
private RingBuffer<LocalSegment3?> _interactorMotionsBuffer
= new RingBuffer<LocalSegment3?>(8);
private RingBuffer<bool> _wasMotionIntersectingBuffer
= new RingBuffer<bool>(8);
#endregion
#region Unity Events
private void Reset() {
if (intObj == null) intObj = GetComponent<InteractionBehaviour>();
if (_localButtonSurface.radii.x == 0 && _localButtonSurface.radii.y == 0) {
_localButtonSurface = new LocalRect(Vector3.zero, Vector2.one * 0.05f);
}
}
private void OnValidate() {
if (intObj == null) intObj = GetComponent<InteractionBehaviour>();
}
private void Start() {
_elementRestPose_hinge = hingeTransform.ToPose().inverse * this.transform.ToPose();
}
private void Update() {
// Calculate information about the current angle of the hinge element relative to
// the hinge resting direction.
_elementAngle = Vector3.SignedAngle(hingeRestAngleDir,
this.transform.rotation * _elementLocalExtensionAxis,
hingeAxis);
// Apply resting forces to the elementAngle.
float? restingAngleCorrection = null;
int restwardPolarity = -_elementAngle > 0 ? 1 : -1;
if (_elementAngle != 0) {
// Rotate the element towards its resting angle.
var restForce = _elementAngle * -1f * restSpringForce;
_restCorrectionVelocity += restForce * Time.deltaTime;
var friction
= -_restCorrectionVelocity * restSpringFriction;
_restCorrectionVelocity += friction * Time.deltaTime;
var dragSign = _restCorrectionVelocity > 0 ? -1 : 1;
var drag = dragSign * _restCorrectionVelocity.Squared() * restSpringDrag;
_restCorrectionVelocity += drag * Time.deltaTime;
if (_restCorrectionVelocity < 0.2f) {
restwardPolarity = 0;
}
restingAngleCorrection = _restCorrectionVelocity * Time.deltaTime;
_elementAngle += restingAngleCorrection.Value;
}
else {
restwardPolarity = 0;
}
// Update interactors.
LocalSegment3? interactorMotion = null;
{
if (_interactorPos.HasValue && _lastInteractorPos.HasValue) {
_lastInteractorPos = Vector3.Lerp(_lastInteractorPos.Value,
_interactorPos.Value,
20f * Time.deltaTime);
}
else {
_lastInteractorPos = _interactorPos;
}
if (restingAngleCorrection.HasValue && _interactorPos.HasValue) {
// Shift the last interactor position by the translation of it produced by
// the resting angle correct. This is akin to computing interactor sweeps in
// hinge-element-local space, where the correction of the hinge element to rest
// accounts for a sweep of the interactor position across its motion.
var rotatedPos = _lastInteractorPos.Value
.RotatedAround(hingeTransform.position,
((float)restingAngleCorrection.Value) * 0.05f,
hingeAxis);
var toRotated = rotatedPos - _lastInteractorPos.Value;
toRotated = toRotated.RotatedBy(Quaternion.AngleAxis(-30f, hingeAxis));
_lastInteractorPos = _lastInteractorPos + toRotated * 20f;
}
_interactorPos = null;
if (intObj.isPrimaryHovered) {
_interactorPos = intObj.primaryHoveringControllerPoint;
}
if (_lastInteractorPos.HasValue && _interactorPos.HasValue) {
var lastToCurrentDir
= (_interactorPos.Value - _lastInteractorPos.Value).normalized;
interactorMotion
= new LocalSegment3(
_lastInteractorPos.Value - lastToCurrentDir * _interactorRadius * 0.01f,
_interactorPos.Value);
}
// Update debug data.
_interactorMotionsBuffer.Add(interactorMotion);
}
// Update interactor collision.
var isSweptInteractorColliding = false;
var isCurrInteractorColliding = false;
var currInteractor = (Sphere?)null;
{
if (interactorMotion.HasValue) {
var segment = interactorMotion.Value;
var motionSqrDist = segment.Intersect(buttonSurface);
if (motionSqrDist < _interactorRadius * _interactorRadius) {
isSweptInteractorColliding = true;
}
}
if (_interactorPos.HasValue) {
currInteractor = new Sphere(_interactorPos.Value, _interactorRadius);
isCurrInteractorColliding = currInteractor.Value.Overlaps(buttonSurface);
}
_wasMotionIntersectingBuffer.Add(isSweptInteractorColliding);
}
// Apply depenetration forces to the elementAngle.
if (currInteractor.HasValue) {
var interactor = new Sphere(_interactorPos.Value, _interactorRadius);
// Determine whether or not we should attempt to hinge the button to depenetrate
// from the interacting sphere geometry.
bool shouldDepenetrate = false;
if (isCurrInteractorColliding || isSweptInteractorColliding) {
shouldDepenetrate = true;
}
// Determine which way the element should depenetrate itself from the interacting
// sphere -- here, we simply depenetrate to the closest side.
var latestPos_button = this.transform.worldToLocalMatrix
.MultiplyPoint3x4(_interactorPos.Value);
var depenetrationPolarity
= Vector3.Dot(latestPos_button, _elementLocalActivationAxis) > 0 ? 1 : -1;
// However, if the button velocity is large enough, we actually should use the
// sign of the curl from its velocity vector about the hinge axis.
if (_lastInteractorPos.HasValue) {
var angleToLastPos = Vector3.SignedAngle(
_lastInteractorPos.Value - _hingeTransform.position,
_hingeTransform.rotation
* Quaternion.AngleAxis((float)_elementAngle, _hingeLocalAxis)
* _elementLocalExtensionAxis,
hingeAxis);
depenetrationPolarity = angleToLastPos > 0 ? 1 : -1;
var velocity = (_lastInteractorPos.Value - _interactorPos.Value).magnitude
/ Time.deltaTime;
if (depenetrationPolarity == restwardPolarity && velocity < 1f) {
shouldDepenetrate = false;
}
}
// TODO: Uncomment this and test with an applicable hinge element (with the
// element extending over the hinge, notice how there is another sign problem,
// where the facing of the element also flips).
//// If the hinging element surface extends onto the other side of the hinge,
//// the correct depenetration direction gets flipped when it is being pushed from
//// that side.
//if (Vector3.Dot(latestPos_button, _elementLocalExtensionAxis) < 0) {
// _depenetrationPolarity = -_depenetrationPolarity;
//}
// If we should depenetrate, we were "interacting" with the button. This is
// publicly-exposed, read-only state.
_isInteracting = shouldDepenetrate;
// Depenetrate the button by moving it on its hinge.
if (shouldDepenetrate) {
var depenetratingAngle
= performTangencyCalculation(interactor, depenetrationPolarity);
_elementAngle += depenetratingAngle;
_restCorrectionVelocity = 0f;
}
}
else {
_isInteracting = false;
}
// Update the transform to reflect the latest angle.
{
// Get the rotation about the hinge axis from the updated hinging element angle.
var elementRotation
= Quaternion.AngleAxis(_elementAngle, _hingeLocalAxis);
// Start with the current hinge pose, rotate it with the element rotation, then
// use the rotated hinge pose to recalculate where the hinging element should be
// relative to it, using the rigid delta-pose that is calculated on Start.
var hingeRotation = _hingeTransform.rotation * elementRotation;
var rotatedHingePose = _hingeTransform.ToPose().WithRotation(hingeRotation);
this.transform.SetPose(rotatedHingePose * _elementRestPose_hinge);
}
}
private float performTangencyCalculation(Sphere interactor, int depenetrationPolarity) {
// To reach tangency:
// Aim at the sphere center, then rotate based on an arcsecant angle calculated
// from the line segment to the sphere center (and sphere radius).
// (There are two possible tangencies to the sphere, so we'll have to pick a
// sign.)
// The hinge plane is the plane defined by the hinge position with a normal
// defined by the axis about which the hinge rotates.
// This will produce an exact solution only if the sphere center is actually
// floating over the hinging element, which, unlike an infinite plane, has a
// bounded extent along the hinge axis.
// This is the sphere position projected on and in the local space of the hinge
// plane.
var interactorPos_hingePlane
= Vector3.ProjectOnPlane((interactor.position - hingePosition),
hingeAxis);
var hingeToCenterAngle
= Vector3.SignedAngle(interactorPos_hingePlane,
elementDir,
hingeAxis);
// Accumulate angle information, to be clamped later.
var aimAtSphereCenterAngle = -hingeToCenterAngle;
// Tangency step. Use distance from sphere and sphere radius.
float d = interactorPos_hingePlane.magnitude;
float r = interactor.radius;
// There is only a solution if the sphere (with its center projected onto the
// hinge plane) is not overlapping the hinge origin.
if (d / r > 1f) {
// Use an identity for arcsec: arcsec(1/x) = arccos(x)
Func<float, float> arcsec = (x) => { return Mathf.Acos(1f / x); };
var theta = arcsec(d / r);
var hingeToTangencyAngle = (90f - theta * Mathf.Rad2Deg)
* depenetrationPolarity;
// Calculate final angle along the hinge.
float depenetratingAngle = aimAtSphereCenterAngle + hingeToTangencyAngle;
// Clamp this angle to based on the bounds of the hinge.
depenetratingAngle = Mathf.Clamp((_elementAngle + depenetratingAngle),
-minMaxHingeAngle.y, -minMaxHingeAngle.x)
- _elementAngle;
// Soften the depenetrating angle so that it depenetrates over multiple
// frames.
var depenetrationStrength = (depenetratingAngle).Map(0f, 90f, 40f, 80f);
depenetratingAngle *= (depenetrationStrength * Time.deltaTime).Clamped01();
return depenetratingAngle;
}
// No depenetration can be calculated.
return 0f;
}
#endregion
#region Runtime Gizmos
public void OnDrawRuntimeGizmos(RuntimeGizmoDrawer drawer) {
if (!drawDebug || !this.enabled || !this.gameObject.activeInHierarchy) return;
drawHingeGizmo(drawer);
//drawBeginPressVolume(drawer); // TODO: DELETEME
drawSweepGizmos(drawer);
drawButtonSurface(drawer);
drawLatestKnownInteractingSphere(drawer);
}
private void drawSweepGizmos(RuntimeGizmoDrawer drawer) {
for (int i = 0; i < _interactorMotionsBuffer.Count; i++) {
var sweep = _interactorMotionsBuffer[i];
var wasSweepIntersecting = _wasMotionIntersectingBuffer[i];
if (sweep.HasValue) {
if (wasSweepIntersecting) {
drawer.color = LeapColor.red;
}
else {
drawer.color = LeapColor.amber;
}
drawer.DrawLine(sweep.Value.a, sweep.Value.b);
}
}
}
private void drawHingeGizmo(RuntimeGizmoDrawer drawer) {
drawer.color = LeapColor.purple;
var currentPlane = this.currentPlane;
float planeCircleRadius = 0.005f;
for (int i = 0; i < 4; i++) {
drawer.DrawWireArc(hingePosition, currentPlane.normal,
currentPlane.normal.Perpendicular(),
planeCircleRadius + i * -0.001f,
1.0f);
}
}
private void drawButtonSurface(RuntimeGizmoDrawer drawer) {
drawer.color = LeapColor.cerulean;
buttonSurface.DrawRuntimeGizmos(drawer);
//drawer.color = LeapColor.white;
//hingedSurface.DrawRuntimeGizmos(drawer);
}
// TODO: DELETEME
//private void drawBeginPressVolume(RuntimeGizmoDrawer drawer) {
// drawer.color = LeapColor.lime.Lerp(Color.white, 0.3f);
// entryVolume.DrawRuntimeGizmos(drawer);
//}
private void drawLatestKnownInteractingSphere(RuntimeGizmoDrawer drawer) {
drawer.color = LeapColor.lime;
if (!_wasMotionIntersectingBuffer.IsEmpty) {
if (_wasMotionIntersectingBuffer.GetLatest()) {
drawer.color = LeapColor.coral;
}
}
if (_interactorPos.HasValue) {
new Sphere(_interactorPos.Value, _interactorRadius).DrawRuntimeGizmos(drawer);
}
if (_lastInteractorPos.HasValue) {
new Sphere(_lastInteractorPos.Value, _interactorRadius).DrawRuntimeGizmos(drawer);
}
}
#endregion
}
#region Extensions
public static class NullableExtensions {
public static U UnwrapDo<T, U>(this T? nullable, Func<T, U> ifHasValue, U defaultIfNull)
where T : struct {
if (nullable.HasValue) {
return ifHasValue(nullable.Value);
}
return defaultIfNull;
}
//public static U UnwrapDo<T, U>(this T refValue, Func<T, U> ifNonNull, U defaultIfNull)
// where T : class {
// if (refValue != null) {
// return ifNonNull(refValue);
// }
// return defaultIfNull;
//}
}
#endregion
}
c30bb7b85This file is pinned to the selected release. Download the original to open it in your design tool.