using System;
|
using System.Collections.Generic;
|
using UnityEngine;
|
using UnityEngine.Events;
|
|
namespace Flexalon
|
{
|
/// <summary> Allows a gameObject to be clicked and dragged. </summary>
|
[AddComponentMenu("Flexalon/Flexalon Interactable"), HelpURL("https://www.flexalon.com/docs/interactable"), DisallowMultipleComponent]
|
public class FlexalonInteractable : MonoBehaviour
|
{
|
[SerializeField]
|
private bool _clickable = false;
|
/// <summary> Determines if this object can be clicked and generate click events. </summary>
|
public bool Clickable {
|
get => _clickable;
|
set => _clickable = value;
|
}
|
|
[SerializeField]
|
private float _maxClickTime = 0.1f;
|
/// <summary>
|
/// 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.
|
/// </summary>
|
public float MaxClickTime {
|
get => _maxClickTime;
|
set => _maxClickTime = value;
|
}
|
|
[SerializeField]
|
private bool _draggable = false;
|
/// <summary> Determines if this object can be dragged and generate drag events. </summary>
|
public bool Draggable {
|
get => _draggable;
|
set => _draggable = value;
|
}
|
|
[SerializeField]
|
private float _interpolationSpeed = 10;
|
/// <summary> How quickly the object moves towards the cursor when dragged. </summary>
|
public float InterpolationSpeed {
|
get => _interpolationSpeed;
|
set => _interpolationSpeed = value;
|
}
|
|
[SerializeField]
|
private float _insertRadius = 0.5f;
|
/// <summary> How close this object needs to a drag target's bounds to be inserted. </summary>
|
public float InsertRadius {
|
get => _insertRadius;
|
set => _insertRadius = value;
|
}
|
|
/// <summary> Restricts the movement of an object during a drag. </summary>
|
public enum RestrictionType
|
{
|
/// <summary> No restriction ensures the object can move freely. </summary>
|
None,
|
|
/// <summary> Plane restriction ensures the object moves along a plane, defined
|
/// by the objects initial position and the Plane Normal property. </summary>
|
Plane,
|
|
/// <summary> Line restriction ensures the object moves along a line, defined
|
/// by the object's initial position and the Line Direction property. </summary>
|
Line
|
}
|
|
[SerializeField]
|
private RestrictionType _restriction = RestrictionType.None;
|
/// <summary> Determines how to restrict the object's drag movement. </summary>
|
public RestrictionType Restriction {
|
get => _restriction;
|
set => _restriction = value;
|
}
|
|
[SerializeField]
|
private Vector3 _planeNormal = Vector3.up;
|
/// <summary> 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. </summary>
|
public Vector3 PlaneNormal {
|
get => _planeNormal;
|
set
|
{
|
_restriction = RestrictionType.Plane;
|
_planeNormal = value;
|
}
|
}
|
|
[SerializeField]
|
private Vector3 _lineDirection = Vector3.right;
|
/// <summary> 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. </summary>
|
public Vector3 LineDirection {
|
get => _lineDirection;
|
set
|
{
|
_restriction = RestrictionType.Line;
|
_lineDirection = value;
|
}
|
}
|
|
[SerializeField]
|
private bool _localSpaceRestriction = true;
|
/// <summary> When checked, the Plane Normal and Line Direction are applied in local space. </summary>
|
public bool LocalSpaceRestriction {
|
get => _localSpaceRestriction;
|
set => _localSpaceRestriction = value;
|
}
|
|
[SerializeField]
|
private Vector3 _holdOffset;
|
// <summary> 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. </summary>
|
public Vector3 HoldOffset {
|
get => _holdOffset;
|
set => _holdOffset = value;
|
}
|
|
[SerializeField]
|
private bool _localSpaceOffset = true;
|
/// <summary> When checked, the Hold Offset is applied in local space. </summary>
|
public bool LocalSpaceOffset {
|
get => _localSpaceOffset;
|
set => _localSpaceOffset = value;
|
}
|
|
[SerializeField]
|
private bool _rotateOnDrag = false;
|
// <summary> 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. </summary>
|
public bool RotateOnDrag {
|
get => _rotateOnDrag;
|
set => _rotateOnDrag = value;
|
}
|
|
[SerializeField]
|
private Quaternion _holdRotation;
|
/// <summary> The rotation to apply to the object when it is being dragged. </summary>
|
public Quaternion HoldRotation {
|
get => _holdRotation;
|
set
|
{
|
_rotateOnDrag = true;
|
_holdRotation = value;
|
}
|
}
|
|
[SerializeField]
|
private bool _localSpaceRotation = true;
|
/// <summary> When checked, the Hold Rotation is applied in local space. </summary>
|
public bool LocalSpaceRotation {
|
get => _localSpaceRotation;
|
set => _localSpaceRotation = value;
|
}
|
|
[SerializeField]
|
private bool _hideCursor = false;
|
/// <summary> When checked, Cursor.visible is set to false when the object is dragged. </summary>
|
public bool HideCursor {
|
get => _hideCursor;
|
set => _hideCursor = value;
|
}
|
|
[SerializeField]
|
private GameObject _handle = null;
|
/// <summary> GameObject to use to select and drag this object. If not set, uses self. </summary>
|
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;
|
/// <summary> If set, the object cannot be dragged outside of the bounds collider. </summary>
|
public Collider Bounds {
|
get => _bounds;
|
set => _bounds = value;
|
}
|
#endif
|
|
[SerializeField]
|
private LayerMask _layerMask = -1;
|
/// <summary> When dragged, limits which Flexalon Drag Targets will accept this object
|
/// by comparing the Layer Mask to the target GameObject's layer. </summary>
|
public LayerMask LayerMask {
|
get => _layerMask;
|
set => _layerMask = value;
|
}
|
|
/// <summary> An event that occurs to a FlexalonInteractable. </summary>
|
[System.Serializable]
|
public class InteractableEvent : UnityEvent<FlexalonInteractable>{}
|
|
[SerializeField]
|
private InteractableEvent _clicked;
|
/// <summary> Unity Event invoked when the object is pressed and released within MaxClickTime. </summary>
|
public InteractableEvent Clicked => _clicked;
|
|
[SerializeField]
|
private InteractableEvent _hoverStart;
|
/// <summary> Unity Event invoked when the object starts being hovered. </summary>
|
public InteractableEvent HoverStart => _hoverStart;
|
|
[SerializeField]
|
private InteractableEvent _hoverEnd;
|
/// <summary> Unity Event invoked when the object stops being hovered. </summary>
|
public InteractableEvent HoverEnd => _hoverEnd;
|
|
[SerializeField]
|
private InteractableEvent _selectStart;
|
/// <summary> Unity Event invoked when the object starts being selected (e.g. press down mouse over object). </summary>
|
public InteractableEvent SelectStart => _selectStart;
|
|
[SerializeField]
|
private InteractableEvent _selectEnd;
|
/// <summary> Unity Event invoked when the object stops being selected (e.g. release mouse). </summary>
|
public InteractableEvent SelectEnd => _selectEnd;
|
|
[SerializeField]
|
private InteractableEvent _dragStart;
|
/// <summary> Unity Event invoked when the object starts being dragged. </summary>
|
public InteractableEvent DragStart => _dragStart;
|
|
[SerializeField]
|
private InteractableEvent _dragEnd;
|
/// <summary> Unity Event invoked when the object stops being dragged. </summary>
|
public InteractableEvent DragEnd => _dragEnd;
|
|
private static List<FlexalonInteractable> _hoveredObjects = new List<FlexalonInteractable>();
|
/// <summary> The currently hovered objects. </summary>
|
public static List<FlexalonInteractable> HoveredObjects => _hoveredObjects;
|
|
/// <summary> The first hovered object. </summary>
|
public static FlexalonInteractable HoveredObject => _hoveredObjects.Count > 0 ? _hoveredObjects[0] : null;
|
|
private static List<FlexalonInteractable> _selectedObjects = new List<FlexalonInteractable>();
|
/// <summary> The currently selected / dragged objects. </summary>
|
public static List<FlexalonInteractable> SelectedObjects => _selectedObjects;
|
|
/// <summary> The first selected / dragged object. </summary>
|
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;
|
|
/// <summary> The current state of the interactable. </summary>
|
public enum InteractableState
|
{
|
/// <summary> The object is not being interacted with. </summary>
|
Init,
|
|
/// <summary> The object is being hovered over. </summary>
|
Hovering,
|
|
/// <summary> The object is being selected (e.g. press down mouse over object). </summary>
|
Selecting,
|
|
/// <summary> The object is being dragged. </summary>
|
Dragging
|
}
|
|
private InteractableState _state = InteractableState.Init;
|
/// <summary> The current state of the interactable. </summary>
|
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<FlexalonDragTarget>() : 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<Canvas>();
|
|
if (_canvas)
|
{
|
if (_restriction == RestrictionType.None)
|
{
|
_restriction = RestrictionType.Plane;
|
_planeNormal = Vector3.forward;
|
}
|
}
|
#endif
|
|
}
|
|
private InputProvider GetInputProvider()
|
{
|
var inputProvider = GetComponent<InputProvider>();
|
if (inputProvider == null)
|
{
|
inputProvider = Flexalon.GetInputProvider();
|
}
|
|
return inputProvider;
|
}
|
|
private void SetState(InteractableState state)
|
{
|
_state = state;
|
}
|
|
private void UpdateState(InputMode inputMode, Ray ray, bool isHit, bool isActive, bool becameActive)
|
{
|
if (_state == InteractableState.Init)
|
{
|
if (isHit && (!isActive || becameActive))
|
{
|
SetState(InteractableState.Hovering);
|
OnHoverStart();
|
}
|
}
|
|
if (_state == InteractableState.Hovering)
|
{
|
if (!isHit)
|
{
|
SetState(InteractableState.Init);
|
OnHoverEnd();
|
}
|
else if (becameActive)
|
{
|
SetState(InteractableState.Selecting);
|
OnSelectStart();
|
}
|
}
|
|
if (_state == InteractableState.Selecting)
|
{
|
if (!isActive)
|
{
|
if (_clickable && isHit && (Time.time - _selectTime <= _maxClickTime))
|
{
|
Clicked.Invoke(this);
|
}
|
|
if (isHit)
|
{
|
SetState(InteractableState.Hovering);
|
OnSelectEnd();
|
}
|
else
|
{
|
SetState(InteractableState.Init);
|
OnSelectEnd();
|
OnHoverEnd();
|
}
|
|
}
|
else if (_draggable && (!_clickable || (Time.time - _selectTime > _maxClickTime)))
|
{
|
SetState(InteractableState.Dragging);
|
OnDragStart(inputMode, ray);
|
}
|
}
|
|
if (_state == InteractableState.Dragging)
|
{
|
if (!isActive)
|
{
|
if (isHit)
|
{
|
SetState(InteractableState.Hovering);
|
OnDragEnd();
|
OnSelectEnd();
|
}
|
else
|
{
|
SetState(InteractableState.Init);
|
OnDragEnd();
|
OnSelectEnd();
|
OnHoverEnd();
|
}
|
}
|
else
|
{
|
OnDragMove(inputMode, ray);
|
}
|
}
|
}
|
|
private void OnHoverStart()
|
{
|
_hoveredObjects.Add(this);
|
HoverStart.Invoke(this);
|
|
// Save this here in case the input provider changes the parent on select.
|
_localSpace = transform.parent;
|
_startSiblingIndex = transform.GetSiblingIndex();
|
}
|
|
private void OnHoverEnd()
|
{
|
_hoveredObjects.Remove(this);
|
HoverEnd.Invoke(this);
|
_localSpace = null;
|
}
|
|
private void OnSelectStart()
|
{
|
_selectTime = Time.time;
|
_selectedObjects.Add(this);
|
SelectStart.Invoke(this);
|
}
|
|
private void OnSelectEnd()
|
{
|
_selectedObjects.Remove(this);
|
SelectEnd.Invoke(this);
|
}
|
|
private void OnDragStart(InputMode inputMode, Ray ray)
|
{
|
if (_hideCursor)
|
{
|
Cursor.visible = false;
|
}
|
|
_target = _lastTarget = transform.position;
|
_clickOffset = transform.position - _raycaster.hitPosition;
|
_distance = Vector3.Distance(_target, ray.origin + _clickOffset);
|
_startPosition = transform.position;
|
|
// Create a placeholder
|
_placeholder = new GameObject("Drag Placeholder");
|
var placeholderObj = Flexalon.AddComponent<FlexalonObject>(_placeholder);
|
_node = Flexalon.GetOrCreateNode(gameObject);
|
placeholderObj.Size = _node.Result.LayoutBounds.size;
|
placeholderObj.Rotation = _node.Rotation;
|
placeholderObj.Scale = _node.Scale;
|
placeholderObj.Margin = _node.Margin;
|
placeholderObj.Padding = _node.Padding;
|
|
_node.IsDragging = true;
|
|
// If we're in a valid drag target, swap with the placeholder.
|
var parentDragTarget = _localSpace ? _localSpace.GetComponent<FlexalonDragTarget>() : null;
|
if (CanAdd(parentDragTarget, parentDragTarget))
|
{
|
MovePlaceholder(_localSpace, _startSiblingIndex);
|
|
// Input provider may be changing the parent before we get here.
|
if (transform.parent == _localSpace)
|
{
|
#if UNITY_UI
|
transform.SetParent(_canvas?.transform, true);
|
#else
|
transform.SetParent(null, true);
|
#endif
|
}
|
}
|
else
|
{
|
_placeholder.transform.SetParent(null);
|
_placeholder.SetActive(false);
|
}
|
|
DragStart.Invoke(this);
|
}
|
|
private void OnDragMove(InputMode inputMode, Ray ray)
|
{
|
if (inputMode == InputMode.External)
|
{
|
_target = transform.position;
|
}
|
else
|
{
|
UpdateTarget(ray);
|
UpdateObjectPosition();
|
}
|
}
|
|
private void OnDragEnd()
|
{
|
_node.IsDragging = false;
|
|
// Swap places with the placeholder and destroy it.
|
if (_placeholder.activeSelf)
|
{
|
transform.SetParent(_placeholder.transform.parent, true);
|
transform.SetSiblingIndex(_placeholder.transform.GetSiblingIndex());
|
}
|
|
_lastValidLocalSpace = null;
|
_placeholder.transform.SetParent(null);
|
Destroy(_placeholder);
|
|
if (_hideCursor)
|
{
|
Cursor.visible = true;
|
}
|
|
DragEnd.Invoke(this);
|
}
|
|
private static bool ClosestPointOnTwoLines(Vector3 p0, Vector3 v0, Vector3 p1, Vector3 v1, out Vector3 closestPointLine2)
|
{
|
closestPointLine2 = Vector3.zero;
|
|
float a = Vector3.Dot(v0, v0);
|
float b = Vector3.Dot(v0, v1);
|
float e = Vector3.Dot(v1, v1);
|
|
float d = a * e - b * b;
|
|
// Lines are not parallel
|
if (d != 0.0f)
|
{
|
Vector3 r = p0 - p1;
|
float c = Vector3.Dot(v0, r);
|
float f = Vector3.Dot(v1, r);
|
float t = (a * f - c * b) / d;
|
closestPointLine2 = p1 + v1 * t;
|
return true;
|
}
|
|
return false;
|
}
|
|
// Sets _target to where we want to move the dragged object -- based on the input ray, restrictions, and bounds.
|
private void UpdateTarget(Ray ray)
|
{
|
ray.origin += _clickOffset;
|
|
if (_restriction == RestrictionType.Line)
|
{
|
var lineDir = _lineDirection;
|
if (_localSpaceRestriction && _lastValidLocalSpace)
|
{
|
lineDir = _lastValidLocalSpace.rotation * _lineDirection;
|
}
|
|
if (!ClosestPointOnTwoLines(ray.origin, ray.direction, _startPosition, lineDir.normalized, out _target))
|
{
|
_target = _startPosition;
|
}
|
}
|
else if (_restriction == RestrictionType.Plane)
|
{
|
var normal = _planeNormal;
|
if (_localSpaceRestriction && _lastValidLocalSpace)
|
{
|
normal = _lastValidLocalSpace.rotation * _planeNormal;
|
}
|
|
_plane.SetNormalAndPosition(normal.normalized, _startPosition);
|
_plane.Raycast(ray, out var distance);
|
_target = ray.origin + ray.direction * distance;
|
}
|
else
|
{
|
// If there's no restriction, just project forward at the same distance as the placeholder.
|
if (_placeholder.gameObject.activeSelf && Flexalon.GetOrCreateNode(_placeholder).HasResult)
|
{
|
_distance = Vector3.Distance(ray.origin, _placeholder.transform.position);
|
}
|
|
_target = ray.origin + ray.direction * _distance;
|
}
|
|
#if UNITY_PHYSICS
|
// Apply bounds restriction
|
if (_bounds && !_bounds.bounds.Contains(_target))
|
{
|
_target = _bounds.ClosestPointOnBounds(_target);
|
}
|
#endif
|
}
|
|
private void UpdateObjectPosition()
|
{
|
// Apply hold offset
|
var offset = Vector3.zero;
|
if (_localSpaceOffset && _localSpace)
|
{
|
offset = _localSpace.localToWorldMatrix.MultiplyVector(_holdOffset);
|
}
|
else if (!_localSpaceOffset)
|
{
|
offset = _holdOffset;
|
}
|
|
// Interpolate object towards target.
|
transform.position = Vector3.Lerp(transform.position, _target + offset, Time.deltaTime * _interpolationSpeed);
|
|
// Apply hold rotation
|
if (_rotateOnDrag)
|
{
|
var rotation = Quaternion.identity;
|
if (_localSpaceRotation && _localSpace)
|
{
|
rotation = _localSpace.rotation * _holdRotation;
|
}
|
else if (!_localSpaceRotation)
|
{
|
rotation = _holdRotation;
|
}
|
|
transform.rotation = Quaternion.Lerp(transform.rotation, rotation, Time.deltaTime * _interpolationSpeed);
|
}
|
}
|
|
private bool TryFindNearestChild(FlexalonDragTarget dragTarget, out Transform nearestChild, out float distanceSquared)
|
{
|
var moveDirection = (_target - _lastTarget).normalized;
|
nearestChild = null;
|
distanceSquared = float.MaxValue;
|
foreach (Transform child in dragTarget.transform)
|
{
|
var childPos = dragTarget.transform.localToWorldMatrix.MultiplyPoint(child.GetComponent<FlexalonResult>().TargetPosition);
|
var toChild = (childPos - _lastTarget).normalized;
|
if (child == _placeholder.transform || Vector3.Dot(toChild, moveDirection) > 0)
|
{
|
var distSq = Vector3.SqrMagnitude(childPos - _target);
|
if (distSq < distanceSquared)
|
{
|
distanceSquared = distSq;
|
nearestChild = child;
|
}
|
}
|
}
|
|
return nearestChild != null;
|
}
|
|
// Find a drag target to insert into by checking if it contains the target point.
|
private bool TryFindNearestDragTarget(FlexalonDragTarget currentDragTarget, out FlexalonDragTarget dragTarget, out Transform nearestChild)
|
{
|
if (!CanLeave(currentDragTarget))
|
{
|
dragTarget = currentDragTarget;
|
TryFindNearestChild(currentDragTarget, out nearestChild, out var distanceSquared);
|
return true;
|
}
|
|
dragTarget = null;
|
nearestChild = null;
|
var minDistance = float.MaxValue;
|
GetInsertPositionAndRadius(_node, _target, out var worldInsertPosition, out var worldInsertRadius);
|
|
foreach (var candidate in FlexalonDragTarget.DragTargets)
|
{
|
if (CanAdd(currentDragTarget, candidate) && candidate.OverlapsSphere(worldInsertPosition, worldInsertRadius))
|
{
|
if (TryFindNearestChild(candidate, out var candidateNearestChild, out var distanceSquared))
|
{
|
if (distanceSquared < minDistance)
|
{
|
minDistance = distanceSquared;
|
dragTarget = candidate;
|
nearestChild = candidateNearestChild;
|
}
|
}
|
else if (dragTarget == null)
|
{
|
dragTarget = candidate;
|
break;
|
}
|
}
|
|
}
|
|
return dragTarget != null;
|
}
|
|
// Moves the placeholder into the drag target at a particular index.
|
private void MovePlaceholder(Transform newParent, int siblingIndex = 0)
|
{
|
if (newParent != _placeholder.transform.parent || siblingIndex != _placeholder.transform.GetSiblingIndex())
|
{
|
_placeholder.SetActive(!!newParent);
|
_placeholder.transform.SetParent(newParent);
|
if (newParent)
|
{
|
_placeholder.transform.SetSiblingIndex(siblingIndex);
|
_lastValidLocalSpace = newParent;
|
}
|
|
_localSpace = newParent;
|
}
|
}
|
|
// Finds an appropriate place to add the placeholder into the drag target.
|
private void AddToLayout(FlexalonDragTarget currentDragTarget, FlexalonDragTarget newDragTarget, Transform nearestChild)
|
{
|
var insertIndex = nearestChild ? nearestChild.GetSiblingIndex() : 0;
|
|
// Special case -- if adding a new item at the end, the user usually wants to place
|
// it after the last element.
|
if (currentDragTarget != newDragTarget && insertIndex == newDragTarget.transform.childCount - 1)
|
{
|
insertIndex++;
|
}
|
|
MovePlaceholder(newDragTarget.transform, insertIndex);
|
}
|
|
private bool CanLeave(FlexalonDragTarget dragTarget)
|
{
|
return dragTarget == null ||
|
(dragTarget.CanRemoveObjects && dragTarget.transform.childCount > dragTarget.MinObjects);
|
}
|
|
private bool CanAdd(FlexalonDragTarget currentDragTarget, FlexalonDragTarget dragTarget)
|
{
|
if (currentDragTarget == dragTarget)
|
{
|
return true;
|
}
|
|
return dragTarget != null &&
|
dragTarget.gameObject != gameObject &&
|
dragTarget.CanAddObjects &&
|
(dragTarget.MaxObjects == 0 || dragTarget.transform.childCount < dragTarget.MaxObjects) &&
|
(_layerMask.value & (1 << dragTarget.gameObject.layer)) != 0;
|
}
|
|
private void GetInsertPositionAndRadius(FlexalonNode node, Vector3 target, out Vector3 position, out float radius)
|
{
|
var worldBoxScale = node.GetWorldBoxScale(true);
|
var scale = transform.lossyScale;
|
radius = _insertRadius * Mathf.Max(scale.x, scale.y, scale.z);
|
position = target;
|
}
|
|
private void OnDrawGizmosSelected()
|
{
|
var node = Flexalon.GetNode(gameObject);
|
if (node != null && _draggable)
|
{
|
Gizmos.color = Color.green;
|
var target = _state == InteractableState.Dragging ? _target : transform.position;
|
GetInsertPositionAndRadius(node, target, out var insertPosition, out var insertRadius);
|
Gizmos.DrawWireSphere(insertPosition, insertRadius);
|
}
|
}
|
}
|
}
|