368 lines
13 KiB
C#
368 lines
13 KiB
C#
/*
|
|
* @author Valentin Simonov / http://va.lent.in/
|
|
*/
|
|
|
|
using System;
|
|
using TouchScript.Gestures;
|
|
using TouchScript.Gestures.TransformGestures;
|
|
using TouchScript.Gestures.TransformGestures.Base;
|
|
using TouchScript.Utils.Attributes;
|
|
using UnityEngine;
|
|
|
|
namespace TouchScript.Behaviors
|
|
{
|
|
/// <summary>
|
|
/// Component which transforms an object according to events from transform gestures: <see cref="TransformGesture"/>, <see cref="ScreenTransformGesture"/>, <see cref="PinnedTransformGesture"/> and others.
|
|
/// </summary>
|
|
[AddComponentMenu("TouchScript/Behaviors/Transformer")]
|
|
[HelpURL("http://touchscript.github.io/docs/html/T_TouchScript_Behaviors_Transformer.htm")]
|
|
public class Transformer : MonoBehaviour
|
|
{
|
|
// Here's how it works.
|
|
//
|
|
// If smoothing is not enabled, the component just gets gesture events in stateChangedHandler(), passes Changed event to manualUpdate() which calls applyValues() to sett updated values.
|
|
// The value of transformMask is used to only set values which were changed not to interfere with scripts changing this values.
|
|
//
|
|
// If smoothing is enabled — targetPosition, targetScale, targetRotation are cached and a lerp from current position to these target positions is applied every frame in update() method. It also checks transformMask to change only needed values.
|
|
// If none of the delta values pass the threshold, the component transitions to idle state.
|
|
|
|
#region Consts
|
|
|
|
/// <summary>
|
|
/// State for internal Transformer state machine.
|
|
/// </summary>
|
|
private enum TransformerState
|
|
{
|
|
/// <summary>
|
|
/// Nothing is happening.
|
|
/// </summary>
|
|
Idle,
|
|
|
|
/// <summary>
|
|
/// The object is under manual control, i.e. user is transforming it.
|
|
/// </summary>
|
|
Manual,
|
|
|
|
/// <summary>
|
|
/// The object is under automatic control, i.e. it's being smoothly moved into target position when user lifted all fingers off.
|
|
/// </summary>
|
|
Automatic
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public properties
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether Smoothing is enabled. Smoothing allows to reduce jagged movements but adds some visual lag.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <c>true</c> if Smoothing is enabled; otherwise, <c>false</c>.
|
|
/// </value>
|
|
public bool EnableSmoothing
|
|
{
|
|
get { return enableSmoothing; }
|
|
set { enableSmoothing = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the smoothing factor.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The smoothing factor. Indicates how much smoothing to apply. 0 - no smoothing, 100000 - maximum.
|
|
/// </value>
|
|
public float SmoothingFactor
|
|
{
|
|
get { return smoothingFactor * 100000f; }
|
|
set { smoothingFactor = Mathf.Clamp(value / 100000f, 0, 1); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the position threshold.
|
|
/// </summary>
|
|
/// <value>
|
|
/// Minimum distance between target position and smoothed position when to stop automatic movement.
|
|
/// </value>
|
|
public float PositionThreshold
|
|
{
|
|
get { return Mathf.Sqrt(positionThreshold); }
|
|
set { positionThreshold = value * value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the rotation threshold.
|
|
/// </summary>
|
|
/// <value>
|
|
/// Minimum angle between target rotation and smoothed rotation when to stop automatic movement.
|
|
/// </value>
|
|
public float RotationThreshold
|
|
{
|
|
get { return rotationThreshold; }
|
|
set { rotationThreshold = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the scale threshold.
|
|
/// </summary>
|
|
/// <value>
|
|
/// Minimum difference between target scale and smoothed scale when to stop automatic movement.
|
|
/// </value>
|
|
public float ScaleThreshold
|
|
{
|
|
get { return Mathf.Sqrt(scaleThreshold); }
|
|
set { scaleThreshold = value * value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this transform can be changed from another script.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <c>true</c> if this transform can be changed from another script; otherwise, <c>false</c>.
|
|
/// </value>
|
|
public bool AllowChangingFromOutside
|
|
{
|
|
get { return allowChangingFromOutside; }
|
|
set { allowChangingFromOutside = value; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Private variables
|
|
|
|
[SerializeField]
|
|
[ToggleLeft]
|
|
private bool enableSmoothing = false;
|
|
|
|
[SerializeField]
|
|
private float smoothingFactor = 1f / 100000f;
|
|
|
|
[SerializeField]
|
|
private float positionThreshold = 0.01f;
|
|
|
|
[SerializeField]
|
|
private float rotationThreshold = 0.1f;
|
|
|
|
[SerializeField]
|
|
private float scaleThreshold = 0.01f;
|
|
|
|
[SerializeField]
|
|
[ToggleLeft]
|
|
private bool allowChangingFromOutside = false;
|
|
|
|
private TransformerState state;
|
|
|
|
private TransformGestureBase gesture;
|
|
private Transform cachedTransform;
|
|
|
|
private TransformGesture.TransformType transformMask;
|
|
private Vector3 targetPosition, targetScale;
|
|
private Quaternion targetRotation;
|
|
|
|
// last* variables are needed to detect when Transform's properties were changed outside of this script
|
|
private Vector3 lastPosition, lastScale;
|
|
private Quaternion lastRotation;
|
|
|
|
#endregion
|
|
|
|
#region Unity methods
|
|
|
|
private void Awake()
|
|
{
|
|
cachedTransform = transform;
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
gesture = GetComponent<TransformGestureBase>();
|
|
gesture.StateChanged += stateChangedHandler;
|
|
TouchManager.Instance.FrameFinished += frameFinishedHandler;
|
|
|
|
stateIdle();
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
if (gesture != null) gesture.StateChanged -= stateChangedHandler;
|
|
if (TouchManager.Instance != null)
|
|
TouchManager.Instance.FrameFinished -= frameFinishedHandler;
|
|
|
|
stateIdle();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region States
|
|
|
|
private void stateIdle()
|
|
{
|
|
var prevState = state;
|
|
setState(TransformerState.Idle);
|
|
|
|
if (enableSmoothing && prevState == TransformerState.Automatic)
|
|
{
|
|
transform.position = lastPosition = targetPosition;
|
|
var newLocalScale = lastScale = targetScale;
|
|
// prevent recalculating colliders when no scale occurs
|
|
if (newLocalScale != transform.localScale) transform.localScale = newLocalScale;
|
|
transform.rotation = lastRotation = targetRotation;
|
|
}
|
|
|
|
transformMask = TransformGesture.TransformType.None;
|
|
}
|
|
|
|
private void stateManual()
|
|
{
|
|
setState(TransformerState.Manual);
|
|
|
|
targetPosition = lastPosition = cachedTransform.position;
|
|
targetRotation = lastRotation = cachedTransform.rotation;
|
|
targetScale = lastScale = cachedTransform.localScale;
|
|
transformMask = TransformGesture.TransformType.None;
|
|
}
|
|
|
|
private void stateAutomatic()
|
|
{
|
|
setState(TransformerState.Automatic);
|
|
|
|
if (!enableSmoothing || transformMask == TransformGesture.TransformType.None) stateIdle();
|
|
}
|
|
|
|
private void setState(TransformerState newState)
|
|
{
|
|
state = newState;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Private functions
|
|
|
|
private void update()
|
|
{
|
|
if (state == TransformerState.Idle) return;
|
|
|
|
if (!enableSmoothing) return;
|
|
|
|
var fraction = 1 - Mathf.Pow(smoothingFactor, Time.unscaledDeltaTime);
|
|
var changed = false;
|
|
|
|
if ((transformMask & TransformGesture.TransformType.Scaling) != 0)
|
|
{
|
|
var scale = transform.localScale;
|
|
if (allowChangingFromOutside)
|
|
{
|
|
// Changed by someone else.
|
|
// Need to make sure to check per component here.
|
|
if (!Mathf.Approximately(scale.x, lastScale.x))
|
|
targetScale.x = scale.x;
|
|
if (!Mathf.Approximately(scale.y, lastScale.y))
|
|
targetScale.y = scale.y;
|
|
if (!Mathf.Approximately(scale.z, lastScale.z))
|
|
targetScale.z = scale.z;
|
|
}
|
|
var newLocalScale = Vector3.Lerp(scale, targetScale, fraction);
|
|
// Prevent recalculating colliders when no scale occurs.
|
|
if (newLocalScale != scale)
|
|
{
|
|
transform.localScale = newLocalScale;
|
|
// Something might have adjusted our scale.
|
|
lastScale = transform.localScale;
|
|
}
|
|
|
|
if (state == TransformerState.Automatic && !changed && (targetScale - lastScale).sqrMagnitude > scaleThreshold) changed = true;
|
|
}
|
|
|
|
if ((transformMask & TransformGesture.TransformType.Rotation) != 0)
|
|
{
|
|
if (allowChangingFromOutside)
|
|
{
|
|
// Changed by someone else.
|
|
if (transform.rotation != lastRotation) targetRotation = transform.rotation;
|
|
}
|
|
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, fraction);
|
|
// Something might have adjusted our rotation.
|
|
lastRotation = transform.rotation;
|
|
|
|
if (state == TransformerState.Automatic && !changed && Quaternion.Angle(targetRotation, lastRotation) > rotationThreshold) changed = true;
|
|
}
|
|
|
|
if ((transformMask & TransformGesture.TransformType.Translation) != 0)
|
|
{
|
|
var pos = transform.position;
|
|
if (allowChangingFromOutside)
|
|
{
|
|
// Changed by someone else.
|
|
// Need to make sure to check per component here.
|
|
if (!Mathf.Approximately(pos.x, lastPosition.x))
|
|
targetPosition.x = pos.x;
|
|
if (!Mathf.Approximately(pos.y, lastPosition.y))
|
|
targetPosition.y = pos.y;
|
|
if (!Mathf.Approximately(pos.z, lastPosition.z))
|
|
targetPosition.z = pos.z;
|
|
}
|
|
transform.position = Vector3.Lerp(pos, targetPosition, fraction);
|
|
// Something might have adjusted our position (most likely Unity UI).
|
|
lastPosition = transform.position;
|
|
|
|
if (state == TransformerState.Automatic && !changed && (targetPosition - lastPosition).sqrMagnitude > positionThreshold) changed = true;
|
|
}
|
|
|
|
if (state == TransformerState.Automatic && !changed) stateIdle();
|
|
}
|
|
|
|
private void manualUpdate()
|
|
{
|
|
if (state != TransformerState.Manual) stateManual();
|
|
|
|
var mask = gesture.TransformMask;
|
|
if ((mask & TransformGesture.TransformType.Scaling) != 0) targetScale *= gesture.DeltaScale;
|
|
if ((mask & TransformGesture.TransformType.Rotation) != 0)
|
|
targetRotation = Quaternion.AngleAxis(gesture.DeltaRotation, gesture.RotationAxis) * targetRotation;
|
|
if ((mask & TransformGesture.TransformType.Translation) != 0) targetPosition += gesture.DeltaPosition;
|
|
transformMask |= mask;
|
|
|
|
gesture.OverrideTargetPosition(targetPosition);
|
|
|
|
if (!enableSmoothing) applyValues();
|
|
}
|
|
|
|
private void applyValues()
|
|
{
|
|
if ((transformMask & TransformGesture.TransformType.Scaling) != 0) cachedTransform.localScale = targetScale;
|
|
if ((transformMask & TransformGesture.TransformType.Rotation) != 0) cachedTransform.rotation = targetRotation;
|
|
if ((transformMask & TransformGesture.TransformType.Translation) != 0) cachedTransform.position = targetPosition;
|
|
transformMask = TransformGesture.TransformType.None;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Event handlers
|
|
|
|
private void stateChangedHandler(object sender, GestureStateChangeEventArgs gestureStateChangeEventArgs)
|
|
{
|
|
switch (gestureStateChangeEventArgs.State)
|
|
{
|
|
case Gesture.GestureState.Possible:
|
|
stateManual();
|
|
break;
|
|
case Gesture.GestureState.Changed:
|
|
manualUpdate();
|
|
break;
|
|
case Gesture.GestureState.Ended:
|
|
case Gesture.GestureState.Cancelled:
|
|
stateAutomatic();
|
|
break;
|
|
case Gesture.GestureState.Failed:
|
|
case Gesture.GestureState.Idle:
|
|
if (gestureStateChangeEventArgs.PreviousState == Gesture.GestureState.Possible) stateAutomatic();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void frameFinishedHandler(object sender, EventArgs eventArgs)
|
|
{
|
|
update();
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
} |