2025-06-04 22:49:37 +08:00
/ *
* @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 ;
2025-08-24 18:59:40 +08:00
var touchManager = TouchManager . Instance ;
if ( touchManager ! = null ) touchManager . FrameFinished + = frameFinishedHandler ;
2025-06-04 22:49:37 +08:00
stateIdle ( ) ;
}
private void OnDisable ( )
{
if ( gesture ! = null ) gesture . StateChanged - = stateChangedHandler ;
2025-08-24 18:59:40 +08:00
var touchManager = TouchManager . Instance ;
if ( touchManager ! = null ) touchManager . FrameFinished - = frameFinishedHandler ;
2025-06-04 22:49:37 +08:00
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
}
}