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;
}
}
}