using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Splines; using Object = UnityEngine.Object; namespace UnityEditor.Splines { /// /// Provides methods to track the selection of spline elements, knots, and tangents. /// `SplineTools` and `SplineHandles` use `SplineSelection` to manage these elements. /// public static class SplineSelection { /// /// Action that is called when the element selection changes. /// public static event Action changed; static readonly HashSet s_ObjectSet = new HashSet(); static readonly HashSet s_SelectedSplineInfo = new HashSet(); static Object[] s_SelectedTargetsBuffer = new Object[0]; static SelectionContext context => SelectionContext.instance; internal static List selection => context.selection; // Tracks selected splines in the SplineReorderableList static List s_SelectedSplines = new (); /// /// The number of elements in the current selection. /// public static int Count => selection.Count; static HashSet s_AdjacentTangentCache = new HashSet(); 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(); } } /// /// Clears the current selection. /// public static void Clear() { if (selection.Count == 0) return; IncrementVersion(); ClearNoUndo(true); } internal static void ClearNoUndo(bool notify) { selection.Clear(); if (notify) NotifySelectionChanged(); } /// /// Checks if the current selection contains at least one element from the given targeted splines. /// /// The splines to consider when looking at selected elements. /// or . /// Returns true if the current selection contains at least an element of the desired type. public static bool HasAny(IReadOnlyList 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; } /// /// Gets the active element of the selection. The active element is generally the last one added to this selection. /// /// The splines to consider when getting the active element. /// The that represents the active knot or tangent. Returns null if no active element is found. public static ISelectableElement GetActiveElement(IReadOnlyList 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; } /// /// Gets all the elements of the current selection, filtered by target splines. Elements are added to the given collection. /// /// The splines to consider when looking at selected elements. /// The collection to fill with spline elements from the selection. /// or . public static void GetElements(IReadOnlyList targets, ICollection 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); } /// /// Gets all the elements of the current selection, from a single spline target. Elements are added to the given collection. /// /// The spline to consider when looking at selected elements. /// The collection to fill with spline elements from the selection. /// or . public static void GetElements(SplineInfo target, ICollection 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(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; } /// /// Used for selecting splines from the inspector, only internal from now. /// internal static IEnumerable SelectedSplines => s_SelectedSplines; /// /// Checks if an element is currently the active one in the selection. /// /// The to test. /// or . /// Returns true if the element is the active element, false if it is not. public static bool IsActive(T element) where T : ISelectableElement { if (selection.Count == 0) return false; return IsEqual(element, selection[0]); } static bool IsEqual(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; } /// /// Sets the active element of the selection. /// /// The to set as the active element. /// or . public static void SetActive(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(); } /// /// Sets the selection to the element. /// /// The to set as the selection. /// or . public static void Set(T element) where T : ISelectableElement { IncrementVersion(); ClearNoUndo(false); selection.Insert(0, new SelectableSplineElement(element)); NotifySelectionChanged(); } internal static void Set(IEnumerable selection) { IncrementVersion(); context.selection.Clear(); context.selection.AddRange(selection); NotifySelectionChanged(); } /// /// Adds an element to the current selection. /// /// The to add to the selection. /// or . /// Returns true if the element was added to the selection, and false if the element is already in the selection. public static bool Add(T element) where T : ISelectableElement { if (Contains(element)) return false; IncrementVersion(); selection.Insert(0, new SelectableSplineElement(element)); NotifySelectionChanged(); return true; } /// /// Add a set of elements to the current selection. /// /// The set of to add to the selection. /// or . public static void AddRange(IEnumerable 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(); } /// /// Remove an element from the current selection. /// /// The to remove from the selection. /// or . /// Returns true if the element has been removed from the selection, false otherwise. public static bool Remove(T element) where T : ISelectableElement { var index = IndexOf(element); if (index >= 0) { IncrementVersion(); selection.RemoveAt(index); NotifySelectionChanged(); return true; } return false; } /// /// Remove a set of elements from the current selection. /// /// The set of to remove from the selection. /// or . /// Returns true if at least an element has been removed from the selection, false otherwise. public static bool RemoveRange(IReadOnlyList 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 element) where T : ISelectableElement { for (int i = 0; i < selection.Count; ++i) if (IsEqual(element, selection[i])) return i; return -1; } /// /// Checks if the selection contains a knot or a tangent.c'est /// /// The element to verify. /// or . /// Returns true if the element is contained in the current selection, false otherwise. public static bool Contains(T element) where T : ISelectableElement { return IndexOf(element) >= 0; } // Used when the selection is changed in the tools. internal static void UpdateObjectSelection(IEnumerable 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 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); } } }