You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
610 lines
23 KiB
610 lines
23 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using UnityEngine.Splines;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace UnityEditor.Splines
|
|
{
|
|
/// <summary>
|
|
/// Provides methods to track the selection of spline elements, knots, and tangents.
|
|
/// `SplineTools` and `SplineHandles` use `SplineSelection` to manage these elements.
|
|
/// </summary>
|
|
public static class SplineSelection
|
|
{
|
|
/// <summary>
|
|
/// Action that is called when the element selection changes.
|
|
/// </summary>
|
|
public static event Action changed;
|
|
|
|
static readonly HashSet<Object> s_ObjectSet = new HashSet<Object>();
|
|
static readonly HashSet<SplineInfo> s_SelectedSplineInfo = new HashSet<SplineInfo>();
|
|
static Object[] s_SelectedTargetsBuffer = new Object[0];
|
|
|
|
static SelectionContext context => SelectionContext.instance;
|
|
internal static List<SelectableSplineElement> selection => context.selection;
|
|
|
|
// Tracks selected splines in the SplineReorderableList
|
|
static List<SplineInfo> s_SelectedSplines = new ();
|
|
|
|
/// <summary>
|
|
/// The number of elements in the current selection.
|
|
/// </summary>
|
|
public static int Count => selection.Count;
|
|
static HashSet<SelectableTangent> s_AdjacentTangentCache = new HashSet<SelectableTangent>();
|
|
|
|
static int s_SelectionVersion;
|
|
|
|
static SplineSelection()
|
|
{
|
|
context.version = 0;
|
|
|
|
Undo.undoRedoPerformed += OnUndoRedoPerformed;
|
|
EditorSplineUtility.knotInserted += OnKnotInserted;
|
|
EditorSplineUtility.knotRemoved += OnKnotRemoved;
|
|
Selection.selectionChanged += OnSelectionChanged;
|
|
}
|
|
|
|
static void OnSelectionChanged()
|
|
{
|
|
ClearInspectorSelectedSplines();
|
|
}
|
|
|
|
static void OnUndoRedoPerformed()
|
|
{
|
|
if (context.version != s_SelectionVersion)
|
|
{
|
|
s_SelectionVersion = context.version;
|
|
ClearInspectorSelectedSplines();
|
|
NotifySelectionChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the current selection.
|
|
/// </summary>
|
|
public static void Clear()
|
|
{
|
|
if (selection.Count == 0)
|
|
return;
|
|
|
|
IncrementVersion();
|
|
ClearNoUndo(true);
|
|
}
|
|
|
|
internal static void ClearNoUndo(bool notify)
|
|
{
|
|
selection.Clear();
|
|
if (notify)
|
|
NotifySelectionChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the current selection contains at least one element from the given targeted splines.
|
|
/// </summary>
|
|
/// <param name="targets">The splines to consider when looking at selected elements.</param>
|
|
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
|
|
/// <returns>Returns true if the current selection contains at least an element of the desired type.</returns>
|
|
public static bool HasAny<T>(IReadOnlyList<SplineInfo> targets)
|
|
where T : struct, ISelectableElement
|
|
{
|
|
for (int i = 0; i < Count; ++i)
|
|
for (int j = 0; j < targets.Count; ++j)
|
|
if (TryGetElement(selection[i], targets[j], out T _))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the active element of the selection. The active element is generally the last one added to this selection.
|
|
/// </summary>
|
|
/// <param name="targets">The splines to consider when getting the active element.</param>
|
|
/// <returns>The <see cref="ISelectableElement"/> that represents the active knot or tangent. Returns null if no active element is found.</returns>
|
|
public static ISelectableElement GetActiveElement(IReadOnlyList<SplineInfo> targets)
|
|
{
|
|
for (int i = 0; i < Count; ++i)
|
|
for (int j = 0; j < targets.Count; ++j)
|
|
if (TryGetElement(selection[i], targets[j], out ISelectableElement result))
|
|
return result;
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all the elements of the current selection, filtered by target splines. Elements are added to the given collection.
|
|
/// </summary>
|
|
/// <param name="targets">The splines to consider when looking at selected elements.</param>
|
|
/// <param name="results">The collection to fill with spline elements from the selection.</param>
|
|
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
|
|
public static void GetElements<T>(IReadOnlyList<SplineInfo> targets, ICollection<T> results)
|
|
where T : ISelectableElement
|
|
{
|
|
results.Clear();
|
|
for (int i = 0; i < Count; ++i)
|
|
for (int j = 0; j < targets.Count; ++j)
|
|
if (TryGetElement(selection[i], targets[j], out T result))
|
|
results.Add(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all the elements of the current selection, from a single spline target. Elements are added to the given collection.
|
|
/// </summary>
|
|
/// <param name="target">The spline to consider when looking at selected elements.</param>
|
|
/// <param name="results">The collection to fill with spline elements from the selection.</param>
|
|
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
|
|
public static void GetElements<T>(SplineInfo target, ICollection<T> results)
|
|
where T : ISelectableElement
|
|
{
|
|
results.Clear();
|
|
for (int i = 0; i < Count; ++i)
|
|
if (TryGetElement(selection[i], target, out T result))
|
|
results.Add(result);
|
|
}
|
|
|
|
static bool TryGetElement<T>(SelectableSplineElement element, SplineInfo splineInfo, out T value)
|
|
where T : ISelectableElement
|
|
{
|
|
if (element.target == splineInfo.Container as Object)
|
|
{
|
|
if (element.targetIndex == splineInfo.Index)
|
|
{
|
|
if (element.tangentIndex >= 0)
|
|
{
|
|
var tangent = new SelectableTangent(splineInfo, element.knotIndex, element.tangentIndex);
|
|
if (tangent.IsValid() && tangent is T t)
|
|
{
|
|
value = t;
|
|
return true;
|
|
}
|
|
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
var knot = new SelectableKnot(splineInfo, element.knotIndex);
|
|
if (knot.IsValid() && knot is T k)
|
|
{
|
|
value = k;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
internal static Object[] GetAllSelectedTargets()
|
|
{
|
|
s_ObjectSet.Clear();
|
|
foreach (var element in selection)
|
|
{
|
|
s_ObjectSet.Add(element.target);
|
|
}
|
|
Array.Resize(ref s_SelectedTargetsBuffer, s_ObjectSet.Count);
|
|
s_ObjectSet.CopyTo(s_SelectedTargetsBuffer);
|
|
return s_SelectedTargetsBuffer;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used for selecting splines from the inspector, only internal from now.
|
|
/// </summary>
|
|
internal static IEnumerable<SplineInfo> SelectedSplines => s_SelectedSplines;
|
|
|
|
/// <summary>
|
|
/// Checks if an element is currently the active one in the selection.
|
|
/// </summary>
|
|
/// <param name="element">The <see cref="ISelectableElement"/> to test.</param>
|
|
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
|
|
/// <returns>Returns true if the element is the active element, false if it is not.</returns>
|
|
public static bool IsActive<T>(T element)
|
|
where T : ISelectableElement
|
|
{
|
|
if (selection.Count == 0)
|
|
return false;
|
|
|
|
return IsEqual(element, selection[0]);
|
|
}
|
|
|
|
static bool IsEqual<T>(T element, SelectableSplineElement selectionData)
|
|
where T : ISelectableElement
|
|
{
|
|
int tangentIndex = element is SelectableTangent tangent ? tangent.TangentIndex : -1;
|
|
return element.SplineInfo.Object == selectionData.target
|
|
&& element.SplineInfo.Index == selectionData.targetIndex
|
|
&& element.KnotIndex == selectionData.knotIndex
|
|
&& tangentIndex == selectionData.tangentIndex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the active element of the selection.
|
|
/// </summary>
|
|
/// <param name="element">The <see cref="ISelectableElement"/> to set as the active element.</param>
|
|
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
|
|
public static void SetActive<T>(T element)
|
|
where T : ISelectableElement
|
|
{
|
|
var index = IndexOf(element);
|
|
if (index == 0)
|
|
return;
|
|
|
|
IncrementVersion();
|
|
|
|
if (index > 0)
|
|
selection.RemoveAt(index);
|
|
|
|
var e = new SelectableSplineElement(element);
|
|
selection.Insert(0, e);
|
|
|
|
if(e.target is Component component)
|
|
{
|
|
//Set the active unity object so the spline is the first target
|
|
Object[] unitySelection = Selection.objects;
|
|
var target = component.gameObject;
|
|
|
|
index = Array.IndexOf(unitySelection, target);
|
|
if(index > 0)
|
|
{
|
|
Object prevObj = unitySelection[0];
|
|
unitySelection[0] = unitySelection[index];
|
|
unitySelection[index] = prevObj;
|
|
Selection.objects = unitySelection;
|
|
}
|
|
}
|
|
|
|
NotifySelectionChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the selection to the element.
|
|
/// </summary>
|
|
/// <param name="element">The <see cref="ISelectableElement"/> to set as the selection.</param>
|
|
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
|
|
public static void Set<T>(T element)
|
|
where T : ISelectableElement
|
|
{
|
|
IncrementVersion();
|
|
|
|
ClearNoUndo(false);
|
|
selection.Insert(0, new SelectableSplineElement(element));
|
|
NotifySelectionChanged();
|
|
}
|
|
|
|
internal static void Set(IEnumerable<SelectableSplineElement> selection)
|
|
{
|
|
IncrementVersion();
|
|
context.selection.Clear();
|
|
context.selection.AddRange(selection);
|
|
NotifySelectionChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an element to the current selection.
|
|
/// </summary>
|
|
/// <param name="element">The <see cref="ISelectableElement"/> to add to the selection.</param>
|
|
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
|
|
/// <returns>Returns true if the element was added to the selection, and false if the element is already in the selection.</returns>
|
|
public static bool Add<T>(T element)
|
|
where T : ISelectableElement
|
|
{
|
|
if (Contains(element))
|
|
return false;
|
|
|
|
IncrementVersion();
|
|
selection.Insert(0, new SelectableSplineElement(element));
|
|
NotifySelectionChanged();
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a set of elements to the current selection.
|
|
/// </summary>
|
|
/// <param name="elements">The set of <see cref="ISelectableElement"/> to add to the selection.</param>
|
|
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
|
|
public static void AddRange<T>(IEnumerable<T> elements)
|
|
where T : ISelectableElement
|
|
{
|
|
bool changed = false;
|
|
foreach (var element in elements)
|
|
{
|
|
if (!Contains(element))
|
|
{
|
|
if (!changed)
|
|
{
|
|
changed = true;
|
|
IncrementVersion();
|
|
}
|
|
|
|
selection.Insert(0, new SelectableSplineElement(element));
|
|
}
|
|
}
|
|
|
|
if (changed)
|
|
NotifySelectionChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove an element from the current selection.
|
|
/// </summary>
|
|
/// <param name="element">The <see cref="ISelectableElement"/> to remove from the selection.</param>
|
|
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
|
|
/// <returns>Returns true if the element has been removed from the selection, false otherwise.</returns>
|
|
public static bool Remove<T>(T element)
|
|
where T : ISelectableElement
|
|
{
|
|
var index = IndexOf(element);
|
|
if (index >= 0)
|
|
{
|
|
IncrementVersion();
|
|
selection.RemoveAt(index);
|
|
NotifySelectionChanged();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove a set of elements from the current selection.
|
|
/// </summary>
|
|
/// <param name="elements">The set of <see cref="ISelectableElement"/> to remove from the selection.</param>
|
|
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
|
|
/// <returns>Returns true if at least an element has been removed from the selection, false otherwise.</returns>
|
|
public static bool RemoveRange<T>(IReadOnlyList<T> elements)
|
|
where T : ISelectableElement
|
|
{
|
|
bool changed = false;
|
|
for (int i = 0; i < elements.Count; ++i)
|
|
{
|
|
var index = IndexOf(elements[i]);
|
|
if (index >= 0)
|
|
{
|
|
if (!changed)
|
|
{
|
|
IncrementVersion();
|
|
changed = true;
|
|
}
|
|
|
|
selection.RemoveAt(index);
|
|
}
|
|
}
|
|
|
|
if (changed)
|
|
NotifySelectionChanged();
|
|
|
|
return changed;
|
|
}
|
|
|
|
static int IndexOf<T>(T element)
|
|
where T : ISelectableElement
|
|
{
|
|
for (int i = 0; i < selection.Count; ++i)
|
|
if (IsEqual(element, selection[i]))
|
|
return i;
|
|
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the selection contains a knot or a tangent.c'est
|
|
/// </summary>
|
|
/// <param name="element">The element to verify.</param>
|
|
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
|
|
/// <returns>Returns true if the element is contained in the current selection, false otherwise.</returns>
|
|
public static bool Contains<T>(T element)
|
|
where T : ISelectableElement
|
|
{
|
|
return IndexOf(element) >= 0;
|
|
}
|
|
|
|
// Used when the selection is changed in the tools.
|
|
internal static void UpdateObjectSelection(IEnumerable<Object> targets)
|
|
{
|
|
s_ObjectSet.Clear();
|
|
foreach (var target in targets)
|
|
if (target != null)
|
|
s_ObjectSet.Add(target);
|
|
|
|
bool changed = false;
|
|
for (int i = Count - 1; i >= 0; --i)
|
|
{
|
|
bool removeElement = false;
|
|
if (!EditorSplineUtility.Exists(selection[i].target as ISplineContainer, selection[i].targetIndex))
|
|
{
|
|
removeElement = true;
|
|
}
|
|
else if (!s_ObjectSet.Contains(selection[i].target))
|
|
{
|
|
ClearInspectorSelectedSplines();
|
|
removeElement = true;
|
|
}
|
|
else if(selection[i].tangentIndex > 0)
|
|
{
|
|
// In the case of a tangent, also check that the tangent is still valid if the spline type
|
|
// or tangent mode has been updated
|
|
var spline = SplineToolContext.GetSpline(selection[i].target, selection[i].targetIndex);
|
|
removeElement = !SplineUtility.AreTangentsModifiable(spline.GetTangentMode(selection[i].knotIndex));
|
|
}
|
|
|
|
if (removeElement)
|
|
{
|
|
if (!changed)
|
|
{
|
|
changed = true;
|
|
IncrementVersion();
|
|
}
|
|
|
|
if (i < selection.Count)
|
|
selection.RemoveAt(i);
|
|
}
|
|
}
|
|
|
|
if (changed)
|
|
{
|
|
RebuildAdjacentCache();
|
|
NotifySelectionChanged();
|
|
}
|
|
}
|
|
|
|
//Used when inserting new elements in spline
|
|
static void OnKnotInserted(SelectableKnot inserted)
|
|
{
|
|
for (var i = 0; i < selection.Count; ++i)
|
|
{
|
|
var knot = selection[i];
|
|
|
|
if (knot.target == inserted.SplineInfo.Object
|
|
&& knot.targetIndex == inserted.SplineInfo.Index
|
|
&& knot.knotIndex >= inserted.KnotIndex)
|
|
{
|
|
++knot.knotIndex;
|
|
selection[i] = knot;
|
|
}
|
|
}
|
|
RebuildAdjacentCache();
|
|
}
|
|
|
|
//Used when deleting an element in spline
|
|
static void OnKnotRemoved(SelectableKnot removed)
|
|
{
|
|
bool changed = false;
|
|
for (var i = selection.Count - 1; i >= 0; --i)
|
|
{
|
|
var knot = selection[i];
|
|
if (knot.target == removed.SplineInfo.Object && knot.targetIndex == removed.SplineInfo.Index)
|
|
{
|
|
if (knot.knotIndex == removed.KnotIndex)
|
|
{
|
|
if (!changed)
|
|
{
|
|
changed = true;
|
|
IncrementVersion();
|
|
}
|
|
selection.RemoveAt(i);
|
|
}
|
|
else if (knot.knotIndex >= removed.KnotIndex)
|
|
{
|
|
--knot.knotIndex;
|
|
selection[i] = knot;
|
|
}
|
|
}
|
|
}
|
|
RebuildAdjacentCache();
|
|
|
|
if (changed)
|
|
NotifySelectionChanged();
|
|
}
|
|
|
|
static void IncrementVersion()
|
|
{
|
|
Undo.RecordObject(context, "Spline Selection Changed");
|
|
|
|
++s_SelectionVersion;
|
|
++context.version;
|
|
}
|
|
|
|
static void NotifySelectionChanged()
|
|
{
|
|
RebuildAdjacentCache();
|
|
changed?.Invoke();
|
|
}
|
|
|
|
static bool TryGetSplineInfo(SelectableSplineElement element, out SplineInfo splineInfo)
|
|
{
|
|
//Checking null in case the object was destroyed
|
|
if (element.target != null && element.target is ISplineContainer container)
|
|
{
|
|
splineInfo = new SplineInfo(container, element.targetIndex);
|
|
return true;
|
|
}
|
|
|
|
splineInfo = default;
|
|
return false;
|
|
}
|
|
|
|
static bool TryCast(SelectableSplineElement element, out SelectableTangent result)
|
|
{
|
|
if (TryGetSplineInfo(element, out var splineInfo) && element.tangentIndex >= 0)
|
|
{
|
|
result = new SelectableTangent(splineInfo, element.knotIndex, element.tangentIndex);
|
|
return true;
|
|
}
|
|
|
|
result = default;
|
|
return false;
|
|
}
|
|
|
|
static bool TryCast(SelectableSplineElement element, out SelectableKnot result)
|
|
{
|
|
if (TryGetSplineInfo(element, out var splineInfo) && element.tangentIndex < 0)
|
|
{
|
|
result = new SelectableKnot(splineInfo, element.knotIndex);
|
|
return true;
|
|
}
|
|
|
|
result = default;
|
|
return false;
|
|
}
|
|
|
|
internal static bool IsSelectedOrAdjacentToSelected(SelectableTangent tangent)
|
|
{
|
|
return s_AdjacentTangentCache.Contains(tangent);
|
|
}
|
|
|
|
static void RebuildAdjacentCache()
|
|
{
|
|
s_AdjacentTangentCache.Clear();
|
|
|
|
foreach(var element in selection)
|
|
{
|
|
SelectableTangent previousOut, currentIn, currentOut, nextIn;
|
|
if(TryCast(element, out SelectableKnot knot))
|
|
EditorSplineUtility.GetAdjacentTangents(knot, out previousOut, out currentIn, out currentOut, out nextIn);
|
|
else if(TryCast(element, out SelectableTangent tangent))
|
|
EditorSplineUtility.GetAdjacentTangents(tangent, out previousOut, out currentIn, out currentOut, out nextIn);
|
|
else
|
|
continue;
|
|
|
|
s_AdjacentTangentCache.Add(previousOut);
|
|
s_AdjacentTangentCache.Add(currentIn);
|
|
s_AdjacentTangentCache.Add(currentOut);
|
|
s_AdjacentTangentCache.Add(nextIn);
|
|
}
|
|
}
|
|
|
|
internal static void ClearInspectorSelectedSplines()
|
|
{
|
|
s_SelectedSplines.Clear();
|
|
}
|
|
|
|
internal static bool HasActiveSplineSelection()
|
|
{
|
|
return s_SelectedSplines.Count > 0;
|
|
}
|
|
|
|
// Inspector spline selection is a one-way operation. It can only be set by the SplineReorderableList. Changes
|
|
// to selected splines in the Scene or Hierarchy will only clear the selected inspector splines.
|
|
internal static void SetInspectorSelectedSplines(SplineContainer container, IEnumerable<int> selected)
|
|
{
|
|
s_SelectedSplines.Clear();
|
|
foreach (var index in selected)
|
|
s_SelectedSplines.Add(new SplineInfo(container, index));
|
|
|
|
IncrementVersion();
|
|
context.selection = selection.Where(x => x.target == container &&
|
|
(selected.Contains(x.targetIndex) || container.KnotLinkCollection.TryGetKnotLinks(new SplineKnotIndex(x.targetIndex, x.knotIndex), out _))).ToList();
|
|
NotifySelectionChanged();
|
|
}
|
|
|
|
internal static bool Contains(SplineInfo info)
|
|
{
|
|
return s_SelectedSplines.Contains(info);
|
|
}
|
|
|
|
internal static bool Remove(SplineInfo info)
|
|
{
|
|
return s_SelectedSplines.Remove(info);
|
|
}
|
|
}
|
|
}
|