using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Unity.Collections; using Unity.Mathematics; namespace UnityEngine.Splines { /// /// The Spline class is a collection of , the closed/open state, and editing representation. /// [Serializable] public class Spline : ISpline, IList { const int k_CurveDistanceLutResolution = 30; [SerializeField] SplineType m_EditModeType = SplineType.Bezier; [SerializeField] List m_Knots = new List(); [SerializeField] List m_LengthsLookupTable = new List(); [SerializeField] float m_Length = -1f; [SerializeField] bool m_Closed; /// /// Return the number of knots. /// public int Count => m_Knots.Count; /// /// Returns true if this Spline is read-only, false if it is mutable. /// public bool IsReadOnly => false; /// /// Invoked in the editor any time a spline property is modified. /// /// /// In the editor this can be invoked many times per-frame. /// Prefer to use when /// working with splines in the editor. /// public event Action changed; #if UNITY_EDITOR internal static Action 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 } /// /// Invoked any time a spline property is modified. /// /// /// In the editor this can be invoked many times per-frame. /// Prefer to use when working /// with splines in the editor. /// 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; } /// /// The SplineType that this spline should be presented as to the user. /// /// /// 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. /// public SplineType EditType { get => m_EditModeType; set { if (m_EditModeType == value) return; m_EditModeType = value; SetDirty(); } } /// /// A collection of . /// public IEnumerable Knots => m_Knots; /// /// Whether the spline is open (has a start and end point) or closed (forms an unbroken loop). /// public bool Closed { get => m_Closed; set { if (m_Closed == value) return; m_Closed = value; SetDirty(); } } /// /// Return the first index of an element matching item. /// /// The knot to locate. /// The zero-based index of the knot, or -1 if not found. public int IndexOf(BezierKnot item) => m_Knots.IndexOf(item); /// /// Insert a at the specified . /// /// The zero-based index to insert the new element. /// The to insert. public void Insert(int index, BezierKnot knot) { m_Knots.Insert(index, knot); m_LengthsLookupTable.Insert(index, null); SetDirty(); } /// /// Removes the knot at the specified index. /// /// The zero-based index of the element to remove. public void RemoveAt(int index) { m_Knots.RemoveAt(index); m_LengthsLookupTable.RemoveAt(index); SetDirty(); } /// /// Get or set the knot at . /// /// The zero-based index of the element to get or set. public BezierKnot this[int index] { get => m_Knots[index]; set { m_Knots[index] = value; SetDirty(); } } /// /// Default constructor creates a spline with no knots, not closed. /// public Spline() { } /// /// Create a spline with a pre-allocated knot capacity. /// /// The capacity of the knot collection. /// Whether the spline is open (has a start and end point) or closed (forms an unbroken loop). public Spline(int knotCapacity, bool closed = false) { m_Knots = new List(knotCapacity); m_Closed = closed; } /// /// Create a spline from a collection of . /// /// A collection of . /// Whether the spline is open (has a start and end point) or closed (forms an unbroken loop). public Spline(IEnumerable knots, bool closed = false) { m_Knots = knots.ToList(); m_Closed = closed; SetDirty(); } /// /// Get a from a knot index. /// /// The knot index that serves as the first control point for this curve. /// /// A formed by the knot at index and the next knot. /// 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]); } /// /// Return the length of a curve. /// /// /// /// /// 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; } /// /// Return the sum of all curve lengths, accounting for state. /// Note that this value is not accounting for transform hierarchy. If you require length in world space use /// . /// /// /// This value is cached. It is recommended to call this once in a non-performance critical path to ensure that /// the cache is valid. /// /// /// /// /// Returns the sum length of all curves composing this spline, accounting for closed state. /// 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]; } /// /// Return the normalized interpolation (t) corresponding to a distance on a . /// /// The zero-based index of the curve. /// The curve-relative distance to convert to an interpolation ratio (also referred to as 't'). /// The normalized interpolation ratio associated to distance on the designated curve. public float GetCurveInterpolation(int curveIndex, float curveDistance) => CurveUtility.GetDistanceToInterpolation(GetCurveDistanceLut(curveIndex), curveDistance); /// /// 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. /// public void Warmup() { var _ = GetLength(); } /// /// Change the size of the list. /// /// The new size of the knots collection. 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(); } /// /// Create an array of spline knots. /// /// Return a new array copy of the knots collection. public BezierKnot[] ToArray() { return m_Knots.ToArray(); } /// /// Copy the values from to this spline. /// /// The Spline to copy property data from. 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(); } /// /// Get an enumerator that iterates through the collection. /// /// An IEnumerator that is used to iterate the collection. public IEnumerator GetEnumerator() => m_Knots.GetEnumerator(); /// IEnumerator IEnumerable.GetEnumerator() => m_Knots.GetEnumerator(); /// /// Adds a knot to the spline. /// /// The to add. public void Add(BezierKnot item) { m_Knots.Add(item); SetDirty(); } /// /// Remove all knots from the spline. /// public void Clear() { m_Knots.Clear(); SetDirty(); } /// /// Return true if a knot is present in the spline. /// /// The to locate. /// Returns true if the knot is found, false if it is not present. public bool Contains(BezierKnot item) => m_Knots.Contains(item); /// /// Copies the contents of the knot list to an array starting at an index. /// /// The destination array to place the copied item in. /// The zero-based index to copy. public void CopyTo(BezierKnot[] array, int arrayIndex) => m_Knots.CopyTo(array, arrayIndex); /// /// Removes the first matching knot. /// /// The to locate and remove. /// Returns true if a matching item was found and removed, false if no match was discovered. public bool Remove(BezierKnot item) { if (m_Knots.Remove(item)) { SetDirty(); return true; } return false; } } }