using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Unity.Collections; using Unity.Mathematics; namespace UnityEngine.Splines { /// <summary> /// The Spline class is a collection of <see cref="BezierKnot"/>, the closed/open state, and editing representation. /// </summary> [Serializable] public class Spline : ISpline, IList<BezierKnot> { const int k_CurveDistanceLutResolution = 30; [SerializeField] SplineType m_EditModeType = SplineType.Bezier; [SerializeField] List<BezierKnot> m_Knots = new List<BezierKnot>(); [SerializeField] List<DistanceToInterpolation[]> m_LengthsLookupTable = new List<DistanceToInterpolation[]>(); [SerializeField] float m_Length = -1f; [SerializeField] bool m_Closed; /// <summary> /// Return the number of knots. /// </summary> public int Count => m_Knots.Count; /// <summary> /// Returns true if this Spline is read-only, false if it is mutable. /// </summary> public bool IsReadOnly => false; /// <summary> /// Invoked in the editor any time a spline property is modified. /// </summary> /// <remarks> /// In the editor this can be invoked many times per-frame. /// Prefer to use <see cref="UnityEditor.Splines.EditorSplineUtility.afterSplineDataWasModified"/> when /// working with splines in the editor. /// </remarks> public event Action changed; #if UNITY_EDITOR internal static Action<Spline> afterSplineWasModified; [NonSerialized] //In the editor, this seemed to be surviving domain reloads bool m_Dirty; #endif internal void SetDirty() { SetLengthCacheDirty(); changed?.Invoke(); OnSplineChanged(); #if UNITY_EDITOR if (m_Dirty) return; m_Dirty = true; UnityEditor.EditorApplication.delayCall += () => { afterSplineWasModified?.Invoke(this); m_Dirty = false; }; #endif } /// <summary> /// Invoked any time a spline property is modified. /// </summary> /// <remarks> /// In the editor this can be invoked many times per-frame. /// Prefer to use <see cref="UnityEditor.Splines.EditorSplineUtility.afterSplineWasModified"/> when working /// with splines in the editor. /// </remarks> protected virtual void OnSplineChanged() { } // todo Remove this and refactor m_Knots to store a struct with knot+cached data void EnsureCurveLengthCacheValid() { if (m_LengthsLookupTable.Count != m_Knots.Count) { m_LengthsLookupTable.Clear(); m_LengthsLookupTable.Capacity = m_Knots.Count; for (int i = 0; i < m_Knots.Count; i++) m_LengthsLookupTable.Add(null); } } // todo Only Catmull Rom requires every curve to be re-evaluated when dirty. // Linear and cubic bezier could be more selective about dirtying cached curve lengths. // Important - This function also serves to enable backwards compatibility with serialized Spline instances // that did not have a length cache. void SetLengthCacheDirty() { EnsureCurveLengthCacheValid(); m_Length = -1f; for (int i = 0; i < m_Knots.Count; i++) m_LengthsLookupTable[i] = null; } /// <summary> /// The SplineType that this spline should be presented as to the user. /// </summary> /// <remarks> /// Internally all splines are stored as a collection of bezier knots, and when editing converted or displayed /// with the handles appropriate to the editable type. /// </remarks> public SplineType EditType { get => m_EditModeType; set { if (m_EditModeType == value) return; m_EditModeType = value; SetDirty(); } } /// <summary> /// A collection of <see cref="BezierKnot"/>. /// </summary> public IEnumerable<BezierKnot> Knots => m_Knots; /// <summary> /// Whether the spline is open (has a start and end point) or closed (forms an unbroken loop). /// </summary> public bool Closed { get => m_Closed; set { if (m_Closed == value) return; m_Closed = value; SetDirty(); } } /// <summary> /// Return the first index of an element matching item. /// </summary> /// <param name="item">The knot to locate.</param> /// <returns>The zero-based index of the knot, or -1 if not found.</returns> public int IndexOf(BezierKnot item) => m_Knots.IndexOf(item); /// <summary> /// Insert a <see cref="BezierKnot"/> at the specified <paramref name="index"/>. /// </summary> /// <param name="index">The zero-based index to insert the new element.</param> /// <param name="knot">The <see cref="BezierKnot"/> to insert.</param> public void Insert(int index, BezierKnot knot) { m_Knots.Insert(index, knot); m_LengthsLookupTable.Insert(index, null); SetDirty(); } /// <summary> /// Removes the knot at the specified index. /// </summary> /// <param name="index">The zero-based index of the element to remove.</param> public void RemoveAt(int index) { m_Knots.RemoveAt(index); m_LengthsLookupTable.RemoveAt(index); SetDirty(); } /// <summary> /// Get or set the knot at <paramref name="index"/>. /// </summary> /// <param name="index">The zero-based index of the element to get or set.</param> public BezierKnot this[int index] { get => m_Knots[index]; set { m_Knots[index] = value; SetDirty(); } } /// <summary> /// Default constructor creates a spline with no knots, not closed. /// </summary> public Spline() { } /// <summary> /// Create a spline with a pre-allocated knot capacity. /// </summary> /// <param name="knotCapacity">The capacity of the knot collection.</param> /// <param name="closed">Whether the spline is open (has a start and end point) or closed (forms an unbroken loop).</param> public Spline(int knotCapacity, bool closed = false) { m_Knots = new List<BezierKnot>(knotCapacity); m_Closed = closed; } /// <summary> /// Create a spline from a collection of <see cref="BezierKnot"/>. /// </summary> /// <param name="knots">A collection of <see cref="BezierKnot"/>.</param> /// <param name="closed">Whether the spline is open (has a start and end point) or closed (forms an unbroken loop).</param> public Spline(IEnumerable<BezierKnot> knots, bool closed = false) { m_Knots = knots.ToList(); m_Closed = closed; SetDirty(); } /// <summary> /// Get a <see cref="BezierCurve"/> from a knot index. /// </summary> /// <param name="index">The knot index that serves as the first control point for this curve.</param> /// <returns> /// A <see cref="BezierCurve"/> formed by the knot at index and the next knot. /// </returns> public BezierCurve GetCurve(int index) { int next = m_Closed ? (index + 1) % m_Knots.Count : math.min(index + 1, m_Knots.Count - 1); return new BezierCurve(m_Knots[index], m_Knots[next]); } /// <summary> /// Return the length of a curve. /// </summary> /// <param name="index"></param> /// <seealso cref="Warmup"/> /// <seealso cref="GetLength"/> /// <returns></returns> public float GetCurveLength(int index) { EnsureCurveLengthCacheValid(); if(m_LengthsLookupTable[index] == null) { m_LengthsLookupTable[index] = new DistanceToInterpolation[k_CurveDistanceLutResolution]; CurveUtility.CalculateCurveLengths(GetCurve(index), m_LengthsLookupTable[index]); } var cumulativeCurveLengths = m_LengthsLookupTable[index]; return cumulativeCurveLengths.Length > 0 ? cumulativeCurveLengths[cumulativeCurveLengths.Length - 1].Distance : 0f; } /// <summary> /// Return the sum of all curve lengths, accounting for <see cref="Closed"/> state. /// Note that this value is not accounting for transform hierarchy. If you require length in world space use /// <see cref="SplineUtility.CalculateLength"/>. /// </summary> /// <remarks> /// This value is cached. It is recommended to call this once in a non-performance critical path to ensure that /// the cache is valid. /// </remarks> /// <seealso cref="Warmup"/> /// <seealso cref="GetCurveLength"/> /// <returns> /// Returns the sum length of all curves composing this spline, accounting for closed state. /// </returns> public float GetLength() { if (m_Length < 0f) { m_Length = 0f; for (int i = 0, c = Closed ? Count : Count - 1; i < c; ++i) m_Length += GetCurveLength(i); } return m_Length; } DistanceToInterpolation[] GetCurveDistanceLut(int index) { if (m_LengthsLookupTable[index] == null) { m_LengthsLookupTable[index] = new DistanceToInterpolation[k_CurveDistanceLutResolution]; CurveUtility.CalculateCurveLengths(GetCurve(index), m_LengthsLookupTable[index]); } return m_LengthsLookupTable[index]; } /// <summary> /// Return the normalized interpolation (t) corresponding to a distance on a <see cref="BezierCurve"/>. /// </summary> /// <param name="curveIndex"> The zero-based index of the curve.</param> /// <param name="curveDistance">The curve-relative distance to convert to an interpolation ratio (also referred to as 't').</param> /// <returns> The normalized interpolation ratio associated to distance on the designated curve.</returns> public float GetCurveInterpolation(int curveIndex, float curveDistance) => CurveUtility.GetDistanceToInterpolation(GetCurveDistanceLut(curveIndex), curveDistance); /// <summary> /// Ensure that all caches contain valid data. Call this to avoid unexpected performance costs when accessing /// spline data. Caches remain valid until any part of the spline state is modified. /// </summary> public void Warmup() { var _ = GetLength(); } /// <summary> /// Change the size of the <see cref="BezierKnot"/> list. /// </summary> /// <param name="newSize">The new size of the knots collection.</param> public void Resize(int newSize) { int count = Count; if (newSize == count) return; if (newSize > count) { while (m_Knots.Count < newSize) { m_Knots.Add(new BezierKnot { Rotation = quaternion.identity }); m_LengthsLookupTable.Add(null); } } else if (newSize < count) { m_Knots.RemoveRange(newSize, m_Knots.Count - newSize); m_LengthsLookupTable.RemoveRange(newSize, m_Knots.Count - newSize); } SetDirty(); } /// <summary> /// Create an array of spline knots. /// </summary> /// <returns>Return a new array copy of the knots collection.</returns> public BezierKnot[] ToArray() { return m_Knots.ToArray(); } /// <summary> /// Copy the values from <paramref name="toCopy"/> to this spline. /// </summary> /// <param name="toCopy">The Spline to copy property data from.</param> public void Copy(Spline toCopy) { if (toCopy == this) return; m_EditModeType = toCopy.m_EditModeType; m_Closed = toCopy.Closed; m_Knots.Clear(); m_Knots.AddRange(toCopy.m_Knots); m_LengthsLookupTable.AddRange(toCopy.m_LengthsLookupTable); SetDirty(); } /// <summary> /// Get an enumerator that iterates through the <see cref="BezierKnot"/> collection. /// </summary> /// <returns>An IEnumerator that is used to iterate the <see cref="BezierKnot"/> collection.</returns> public IEnumerator<BezierKnot> GetEnumerator() => m_Knots.GetEnumerator(); /// <inheritdoc cref="GetEnumerator"/> IEnumerator IEnumerable.GetEnumerator() => m_Knots.GetEnumerator(); /// <summary> /// Adds a knot to the spline. /// </summary> /// <param name="item">The <see cref="BezierKnot"/> to add.</param> public void Add(BezierKnot item) { m_Knots.Add(item); SetDirty(); } /// <summary> /// Remove all knots from the spline. /// </summary> public void Clear() { m_Knots.Clear(); SetDirty(); } /// <summary> /// Return true if a knot is present in the spline. /// </summary> /// <param name="item">The <see cref="BezierKnot"/> to locate.</param> /// <returns>Returns true if the knot is found, false if it is not present.</returns> public bool Contains(BezierKnot item) => m_Knots.Contains(item); /// <summary> /// Copies the contents of the knot list to an array starting at an index. /// </summary> /// <param name="array">The destination array to place the copied item in.</param> /// <param name="arrayIndex">The zero-based index to copy.</param> public void CopyTo(BezierKnot[] array, int arrayIndex) => m_Knots.CopyTo(array, arrayIndex); /// <summary> /// Removes the first matching knot. /// </summary> /// <param name="item">The <see cref="BezierKnot"/> to locate and remove.</param> /// <returns>Returns true if a matching item was found and removed, false if no match was discovered.</returns> public bool Remove(BezierKnot item) { if (m_Knots.Remove(item)) { SetDirty(); return true; } return false; } } }