using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
namespace Flexalon
{
/// Allows a gameObject to be clicked and dragged.
[AddComponentMenu("Flexalon/Flexalon Interactable"), HelpURL("https://www.flexalon.com/docs/interactable"), DisallowMultipleComponent]
public class FlexalonInteractable : MonoBehaviour
{
[SerializeField]
private bool _clickable = false;
/// Determines if this object can be clicked and generate click events.
public bool Clickable {
get => _clickable;
set => _clickable = value;
}
[SerializeField]
private float _maxClickTime = 0.1f;
///
/// With a mouse or touch input, a click is defined as a press and release.
/// The time between press and release must be less than Max Click Time to
/// count as a click. A drag interaction cannot start until Max Click Time is exceeded.
///
public float MaxClickTime {
get => _maxClickTime;
set => _maxClickTime = value;
}
[SerializeField]
private bool _draggable = false;
/// Determines if this object can be dragged and generate drag events.
public bool Draggable {
get => _draggable;
set => _draggable = value;
}
[SerializeField]
private float _interpolationSpeed = 10;
/// How quickly the object moves towards the cursor when dragged.
public float InterpolationSpeed {
get => _interpolationSpeed;
set => _interpolationSpeed = value;
}
[SerializeField]
private float _insertRadius = 0.5f;
/// How close this object needs to a drag target's bounds to be inserted.
public float InsertRadius {
get => _insertRadius;
set => _insertRadius = value;
}
/// Restricts the movement of an object during a drag.
public enum RestrictionType
{
/// No restriction ensures the object can move freely.
None,
/// Plane restriction ensures the object moves along a plane, defined
/// by the objects initial position and the Plane Normal property.
Plane,
/// Line restriction ensures the object moves along a line, defined
/// by the object's initial position and the Line Direction property.
Line
}
[SerializeField]
private RestrictionType _restriction = RestrictionType.None;
/// Determines how to restrict the object's drag movement.
public RestrictionType Restriction {
get => _restriction;
set => _restriction = value;
}
[SerializeField]
private Vector3 _planeNormal = Vector3.up;
/// Defines the normal of the plane when using a plane restriction.
/// If 'Local Space' is checked, this normal is rotated by the transform
/// of the layout that the object started in.
public Vector3 PlaneNormal {
get => _planeNormal;
set
{
_restriction = RestrictionType.Plane;
_planeNormal = value;
}
}
[SerializeField]
private Vector3 _lineDirection = Vector3.right;
/// Defines the direction of the line when using a line restriction.
/// If 'Local Space'is checked, this direction is rotated by the transform
/// of the layout that the object started in.
public Vector3 LineDirection {
get => _lineDirection;
set
{
_restriction = RestrictionType.Line;
_lineDirection = value;
}
}
[SerializeField]
private bool _localSpaceRestriction = true;
/// When checked, the Plane Normal and Line Direction are applied in local space.
public bool LocalSpaceRestriction {
get => _localSpaceRestriction;
set => _localSpaceRestriction = value;
}
[SerializeField]
private Vector3 _holdOffset;
// When dragged, this option adds an offset to the dragged object's position.
// This can be used to float the object near the layout while it is being dragged.
// If 'Local Space' is checked, this offset is rotated and scaled by the transform
// of the layout that the object started in.
public Vector3 HoldOffset {
get => _holdOffset;
set => _holdOffset = value;
}
[SerializeField]
private bool _localSpaceOffset = true;
/// When checked, the Hold Offset is applied in local space.
public bool LocalSpaceOffset {
get => _localSpaceOffset;
set => _localSpaceOffset = value;
}
[SerializeField]
private bool _rotateOnDrag = false;
// When dragged, this option adds a rotation to the dragged object.
// This can be used to tilt the object while it is being dragged.
// If 'Local Space' is checked, this rotation will be in the local
// space of the layout that the object started in.
public bool RotateOnDrag {
get => _rotateOnDrag;
set => _rotateOnDrag = value;
}
[SerializeField]
private Quaternion _holdRotation;
/// The rotation to apply to the object when it is being dragged.
public Quaternion HoldRotation {
get => _holdRotation;
set
{
_rotateOnDrag = true;
_holdRotation = value;
}
}
[SerializeField]
private bool _localSpaceRotation = true;
/// When checked, the Hold Rotation is applied in local space.
public bool LocalSpaceRotation {
get => _localSpaceRotation;
set => _localSpaceRotation = value;
}
[SerializeField]
private bool _hideCursor = false;
/// When checked, Cursor.visible is set to false when the object is dragged.
public bool HideCursor {
get => _hideCursor;
set => _hideCursor = value;
}
[SerializeField]
private GameObject _handle = null;
/// GameObject to use to select and drag this object. If not set, uses self.
public GameObject Handle {
get => _handle;
set
{
_raycaster.Unregister(this);
_handle = value;
_raycaster.Register(this);
}
}
#if UNITY_PHYSICS
[SerializeField, Obsolete("Use Handle instead.")]
private Collider _collider;
void OnValidate()
{
#pragma warning disable 618
if (_collider && !_handle)
{
_handle = _collider.gameObject;
_collider = null;
}
#pragma warning restore 618
}
[SerializeField]
private Collider _bounds;
/// If set, the object cannot be dragged outside of the bounds collider.
public Collider Bounds {
get => _bounds;
set => _bounds = value;
}
#endif
[SerializeField]
private LayerMask _layerMask = -1;
/// When dragged, limits which Flexalon Drag Targets will accept this object
/// by comparing the Layer Mask to the target GameObject's layer.
public LayerMask LayerMask {
get => _layerMask;
set => _layerMask = value;
}
/// An event that occurs to a FlexalonInteractable.
[System.Serializable]
public class InteractableEvent : UnityEvent{}
[SerializeField]
private InteractableEvent _clicked;
/// Unity Event invoked when the object is pressed and released within MaxClickTime.
public InteractableEvent Clicked => _clicked;
[SerializeField]
private InteractableEvent _hoverStart;
/// Unity Event invoked when the object starts being hovered.
public InteractableEvent HoverStart => _hoverStart;
[SerializeField]
private InteractableEvent _hoverEnd;
/// Unity Event invoked when the object stops being hovered.
public InteractableEvent HoverEnd => _hoverEnd;
[SerializeField]
private InteractableEvent _selectStart;
/// Unity Event invoked when the object starts being selected (e.g. press down mouse over object).
public InteractableEvent SelectStart => _selectStart;
[SerializeField]
private InteractableEvent _selectEnd;
/// Unity Event invoked when the object stops being selected (e.g. release mouse).
public InteractableEvent SelectEnd => _selectEnd;
[SerializeField]
private InteractableEvent _dragStart;
/// Unity Event invoked when the object starts being dragged.
public InteractableEvent DragStart => _dragStart;
[SerializeField]
private InteractableEvent _dragEnd;
/// Unity Event invoked when the object stops being dragged.
public InteractableEvent DragEnd => _dragEnd;
private static List _hoveredObjects = new List();
/// The currently hovered objects.
public static List HoveredObjects => _hoveredObjects;
/// The first hovered object.
public static FlexalonInteractable HoveredObject => _hoveredObjects.Count > 0 ? _hoveredObjects[0] : null;
private static List _selectedObjects = new List();
/// The currently selected / dragged objects.
public static List SelectedObjects => _selectedObjects;
/// The first selected / dragged object.
public static FlexalonInteractable SelectedObject => _selectedObjects.Count > 0 ? _selectedObjects[0] : null;
private Vector3 _target;
private Vector3 _lastTarget;
private float _distance;
private GameObject _placeholder;
private Vector3 _startPosition;
private int _startSiblingIndex;
private UnityEngine.Plane _plane = new UnityEngine.Plane();
private static FlexalonRaycaster _raycaster = new FlexalonRaycaster();
private Transform _localSpace;
private Transform _lastValidLocalSpace;
private float _selectTime;
private Vector3 _clickOffset;
private InputProvider _inputProvider;
private FlexalonNode _node;
private bool _wasActive;
#if UNITY_UI
private Canvas _canvas;
internal Canvas Canvas => _canvas;
#endif
// For Editor
internal bool _showAllDragProperties => GetInputProvider().InputMode == InputMode.Raycast;
/// The current state of the interactable.
public enum InteractableState
{
/// The object is not being interacted with.
Init,
/// The object is being hovered over.
Hovering,
/// The object is being selected (e.g. press down mouse over object).
Selecting,
/// The object is being dragged.
Dragging
}
private InteractableState _state = InteractableState.Init;
/// The current state of the interactable.
public InteractableState State => _state;
void Awake()
{
if (_clicked == null)
{
_clicked = new InteractableEvent();
}
if (_hoverStart == null)
{
_hoverStart = new InteractableEvent();
}
if (_hoverEnd == null)
{
_hoverEnd = new InteractableEvent();
}
if (_selectStart == null)
{
_selectStart = new InteractableEvent();
}
if (_selectEnd == null)
{
_selectEnd = new InteractableEvent();
}
if (_dragStart == null)
{
_dragStart = new InteractableEvent();
}
if (_dragEnd == null)
{
_dragEnd = new InteractableEvent();
}
}
void OnEnable()
{
_node = Flexalon.GetOrCreateNode(gameObject);
_inputProvider = GetInputProvider();
UpdateCanvas();
if (!_handle)
{
_handle = gameObject;
}
_raycaster.Register(this);
}
void OnDisable()
{
_raycaster.Unregister(this);
if (_state != InteractableState.Init)
{
UpdateState(_inputProvider.InputMode, default, false, false, false);
}
_node = null;
}
void Update()
{
var inputMode = _inputProvider.InputMode;
Vector3 uiPointer = _inputProvider.UIPointer;
Ray ray = _inputProvider.Ray;
bool isHit = false;
bool isActive = _inputProvider.Active;
bool becameActive = isActive && !_wasActive;
_wasActive = isActive;
if (inputMode == InputMode.Raycast)
{
if (_selectedObjects.Count == 0 || _selectedObjects[0] == this)
{
isHit = _raycaster.IsHit(uiPointer, ray, this);
}
}
else
{
var focusedObject = _inputProvider.ExternalFocusedObject;
isHit = focusedObject && focusedObject == gameObject;
}
#if UNITY_UI
if (_canvas && _canvas.renderMode == RenderMode.ScreenSpaceOverlay)
{
ray = new Ray(uiPointer, Vector3.forward);
}
#endif
UpdateState(inputMode, ray, isHit, isActive, becameActive);
}
void FixedUpdate()
{
if (_state != InteractableState.Dragging)
{
return;
}
if (_target == _lastTarget)
{
return;
}
var currentDragTarget = _placeholder.transform.parent ? _placeholder.transform.parent.GetComponent() : null;
// Find a drag target to insert into.
if (TryFindNearestDragTarget(currentDragTarget, out var newDragTarget, out var nearestChild))
{
AddToLayout(currentDragTarget, newDragTarget, nearestChild);
}
else
{
MovePlaceholder(null);
}
_lastTarget = _target;
}
internal void UpdateCanvas()
{
#if UNITY_UI
if (_canvas)
{
return;
}
_canvas = GetComponentInParent