using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.Mathematics;
namespace UnityEngine.Splines
{
///
/// To calculate a value at some distance along a spline, interpolation is required. The IInterpolator interface
/// allows you to define how data is interpreted given a start value, end value, and normalized interpolation value
/// (commonly referred to as 't').
///
///
/// The data type to interpolate.
///
public interface IInterpolator
{
///
/// Calculate a value between from and to at time interval.
///
/// The starting value. At t = 0 this method should return an unmodified 'from' value.
/// The ending value. At t = 1 this method should return an unmodified 'to' value.
/// A percentage between 'from' and 'to'. Must be between 0 and 1.
/// A value between 'from' and 'to'.
T Interpolate(T from, T to, float t);
}
///
/// Describes the unit of measurement used by .
///
public enum PathIndexUnit
{
///
/// The 't' value used when interpolating is measured in game units. Values range from 0 (start of Spline) to
/// (end of Spline).
///
Distance,
///
/// The 't' value used when interpolating is normalized. Values range from 0 (start of Spline) to 1 (end of Spline).
///
Normalized,
///
/// The 't' value used when interpolating is defined by knot indices and a fractional value representing the
/// normalized interpolation between the specific knot index and the next knot.
///
Knot
}
///
/// The SplineData{T} class is used to store information relative to a without coupling data
/// directly to the Spline class. SplineData can store any type of data, and provides options for how to index
/// DataPoints.
///
/// The type of data to store.
[Serializable]
public class SplineData : IEnumerable>
{
static readonly DataPointComparer> k_DataPointComparer = new DataPointComparer>();
[SerializeField]
PathIndexUnit m_IndexUnit = PathIndexUnit.Knot;
[SerializeField]
List> m_DataPoints = new List>();
// When working with IMGUI it's necessary to keep keys array consistent while a hotcontrol is active. Most
// accessors will keep the SplineData sorted, but sometimes it's not possible.
[NonSerialized]
bool m_NeedsSort;
///
/// Access a by index. DataPoints are sorted in ascending order by the
/// value.
///
///
/// The index of the DataPoint to access.
///
public DataPoint this[int index]
{
get => m_DataPoints[index];
set => SetDataPoint(index, value);
}
///
/// PathIndexUnit defines how SplineData will interpret 't' values when interpolating data.
///
///
public PathIndexUnit PathIndexUnit
{
get => m_IndexUnit;
set => m_IndexUnit = value;
}
///
/// How many data points the SplineData collection contains.
///
public int Count => m_DataPoints.Count;
///
/// The DataPoint Indexes of the current SplineData.
///
public IEnumerable Indexes => m_DataPoints.Select(dp => dp.Index);
///
/// Invoked any time a SplineData 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
bool m_Dirty = false;
internal static Action> afterSplineDataWasModified;
#endif
///
/// Create a new SplineData instance.
///
public SplineData() {}
///
/// Create a new SplineData instance with a single value in it.
///
///
/// A single value to add to the spline data at t = 0.`
///
public SplineData(T init)
{
Add(0f, init);
SetDirty();
}
///
/// Create a new SplineData instance and initialize it with a collection of data points. DataPoints will be sorted and stored
/// in ascending order by .
///
///
/// A collection of DataPoints to initialize SplineData.`
///
public SplineData(IEnumerable> dataPoints)
{
foreach(var dataPoint in dataPoints)
Add(dataPoint);
SetDirty();
}
void SetDirty()
{
changed?.Invoke();
#if UNITY_EDITOR
if(m_Dirty)
return;
m_Dirty = true;
UnityEditor.EditorApplication.delayCall += () =>
{
afterSplineDataWasModified?.Invoke(this);
m_Dirty = false;
};
#endif
}
///
/// Append a to this collection.
///
///
/// The interpolant relative to Spline. How this value is interpreted is dependent on .
///
///
/// The data to store in the created data point.
///
public void Add(float t, T data) => Add(new DataPoint(t, data));
///
/// Append a to this collection.
///
///
/// The data point to append to the SplineData collection.
///
///
/// The index of the inserted dataPoint.
///
public int Add(DataPoint dataPoint)
{
int index = m_DataPoints.BinarySearch(0, Count, dataPoint, k_DataPointComparer);
index = index < 0 ? ~index : index;
m_DataPoints.Insert(index, dataPoint);
SetDirty();
return index;
}
///
/// Append a with default value to this collection.
///
///
/// The interpolant relative to Spline. How this value is interpreted is dependent on .
///
///
/// The index of the inserted dataPoint.
///
public int AddDataPointWithDefaultValue(float t)
{
var dataPoint = new DataPoint() { Index = t };
if(Count == 0)
return Add(dataPoint);
if(Count == 1)
{
dataPoint.Value = m_DataPoints[0].Value;
return Add(dataPoint);
}
int index = m_DataPoints.BinarySearch(0, Count, dataPoint, k_DataPointComparer);
index = index < 0 ? ~index : index;
dataPoint.Value = index == 0 ? m_DataPoints[0].Value : m_DataPoints[index-1].Value;
m_DataPoints.Insert(index, dataPoint);
SetDirty();
return index;
}
///
/// Remove a at index.
///
/// The index to remove.
public void RemoveAt(int index)
{
if (index < 0 || index >= Count)
throw new ArgumentOutOfRangeException(nameof(index));
m_DataPoints.RemoveAt(index);
SetDirty();
}
///
/// Remove a from this collection, if one exists.
///
///
/// The interpolant relative to Spline. How this value is interpreted is dependent on .
///
///
/// True is deleted, false otherwise.
///
public bool RemoveDataPoint(float t)
{
var removed = m_DataPoints.Remove(m_DataPoints.FirstOrDefault(point => Mathf.Approximately(point.Index, t)));
if(removed)
SetDirty();
return removed;
}
///
/// Move a (if it exists) from this collection, from one index to the another.
///
/// The index of the to move.
/// The new index for this .
/// The index of the modified .
///
public int MoveDataPoint(int index, float newIndex)
{
if (index < 0 || index >= Count)
throw new ArgumentOutOfRangeException(nameof(index));
var dataPoint = m_DataPoints[index];
if(Mathf.Approximately(newIndex, dataPoint.Index))
return index;
RemoveAt(index);
dataPoint.Index = newIndex;
int newRealIndex = Add(dataPoint);
return newRealIndex;
}
///
/// Remove all data points.
///
public void Clear()
{
m_DataPoints.Clear();
SetDirty();
}
static int Wrap(int value, int lowerBound, int upperBound)
{
int range_size = upperBound - lowerBound + 1;
if(value < lowerBound)
value += range_size * ( ( lowerBound - value ) / range_size + 1 );
return lowerBound + ( value - lowerBound ) % range_size;
}
int ResolveBinaryIndex(int index, bool wrap)
{
index = ( index < 0 ? ~index : index ) - 1;
if(wrap)
index = Wrap(index, 0, Count - 1);
return math.clamp(index, 0, Count - 1);
}
(int, int, float) GetIndex(float t, float splineLength, int knotCount, bool closed)
{
if(Count < 1)
return default;
SortIfNecessary();
float splineLengthInIndexUnits = splineLength;
if(m_IndexUnit == PathIndexUnit.Normalized)
splineLengthInIndexUnits = 1f;
else if(m_IndexUnit == PathIndexUnit.Knot)
splineLengthInIndexUnits = closed ? knotCount : knotCount - 1;
float maxDataPointTime = m_DataPoints[m_DataPoints.Count - 1].Index;
float maxRevolutionLength = math.ceil(maxDataPointTime / splineLengthInIndexUnits) * splineLengthInIndexUnits;
float maxTime = closed ? math.max(maxRevolutionLength, splineLengthInIndexUnits) : splineLengthInIndexUnits;
if(closed)
{
if(t < 0f)
t = maxTime + t % maxTime;
else
t = t % maxTime;
}
else
t = math.clamp(t, 0f, maxTime);
int index = m_DataPoints.BinarySearch(0, Count, new DataPoint(t, default), k_DataPointComparer);
int fromIndex = ResolveBinaryIndex(index, closed);
int toIndex = closed ? ( fromIndex + 1 ) % Count : math.clamp(fromIndex + 1, 0, Count - 1);
float fromTime = m_DataPoints[fromIndex].Index;
float toTime = m_DataPoints[toIndex].Index;
if(fromIndex > toIndex)
toTime += maxTime;
if(t < fromTime && closed)
t += maxTime;
if(fromTime == toTime)
return ( fromIndex, toIndex, fromTime );
return ( fromIndex, toIndex, math.abs(math.max(0f, t - fromTime) / ( toTime - fromTime )) );
}
///
/// Calculate an interpolated value at a given 't' along a spline.
///
/// The Spline to interpolate.
/// The interpolator value. How this is interpreted is defined by .
/// The that is represented as.
/// The to use. A collection of commonly used
/// interpolators are available in the namespace.
/// The IInterpolator type.
/// The Spline type.
/// An interpolated value.
public T Evaluate(TSpline spline, float t, PathIndexUnit indexUnit, TInterpolator interpolator)
where TSpline : ISpline
where TInterpolator : IInterpolator
{
if(indexUnit == m_IndexUnit)
return Evaluate(spline, t, interpolator);
return Evaluate(spline, SplineUtility.ConvertIndexUnit(spline, t, indexUnit, m_IndexUnit), interpolator);
}
///
/// Calculate an interpolated value at a given 't' along a spline.
///
/// The Spline to interpolate.
/// The interpolator value. How this is interpreted is defined by .
/// The to use. A collection of commonly used
/// interpolators are available in the namespace.
/// The IInterpolator type.
/// The Spline type.
/// An interpolated value.
public T Evaluate(TSpline spline, float t, TInterpolator interpolator)
where TSpline : ISpline
where TInterpolator : IInterpolator
{
var knotCount = spline.Count;
if(knotCount < 1 || m_DataPoints.Count == 0)
return default;
var indices = GetIndex(t, spline.GetLength(), knotCount, spline.Closed);
DataPoint a = m_DataPoints[indices.Item1];
DataPoint b = m_DataPoints[indices.Item2];
return interpolator.Interpolate(a.Value, b.Value, indices.Item3);
}
///
/// Set the data for a at an index.
///
/// The DataPoint index.
/// The value to set.
///
/// Using this method will search the DataPoint list and invoke the
/// callback every time. This may be inconvenient when setting multiple DataPoints during the same frame.
/// In this case, consider calling for each DataPoint, followed by
/// a single call to . Note that the call to is
/// optional and can be omitted if DataPoint sorting is not required and the callback
/// should not be invoked.
///
public void SetDataPoint(int index, DataPoint value)
{
if(index < 0 || index >= Count)
throw new ArgumentOutOfRangeException("index");
RemoveAt(index);
Add(value);
SetDirty();
}
///
/// Set the data for a at an index.
///
/// The DataPoint index.
/// The value to set.
///
/// Use this method as an altenative to when manual control
/// over DataPoint sorting and the callback is required.
/// See also .
///
public void SetDataPointNoSort(int index, DataPoint value)
{
if(index < 0 || index >= Count)
throw new ArgumentOutOfRangeException("index");
// could optimize this by storing affected range
m_NeedsSort = true;
m_DataPoints[index] = value;
}
///
/// Triggers sorting of the list if the data is dirty.
///
///
/// Call this after a single or series of calls to .
/// This will trigger DataPoint sort and invoke the callback.
/// This method has two main use cases: to prevent frequent callback
/// calls within the same frame and to reduce multiple DataPoints list searches
/// to a single sort in performance critical paths.
///
public void SortIfNecessary()
{
if(!m_NeedsSort)
return;
m_NeedsSort = false;
m_DataPoints.Sort();
SetDirty();
}
internal void ForceSort()
{
m_NeedsSort = true;
SortIfNecessary();
}
///
/// Given a spline and a target PathIndex Unit, convert the SplineData to a new PathIndexUnit without changing the final positions on the Spline.
///
/// The Spline type.
/// The Spline to use for the conversion, this is necessary to compute most of PathIndexUnits.
/// The unit to convert SplineData to.>
public void ConvertPathUnit(TSplineType spline, PathIndexUnit toUnit)
where TSplineType : ISpline
{
if(toUnit == m_IndexUnit)
return;
for(int i = 0; i(newTime, dataPoint.Value);
}
m_IndexUnit = toUnit;
SetDirty();
}
///
/// Given a time value using a certain PathIndexUnit type, calculate the normalized time value regarding a specific spline.
///
/// The Spline to use for the conversion, this is necessary to compute Normalized and Distance PathIndexUnits.
/// The time to normalize in the original PathIndexUnit.>
/// The Spline type.
/// The normalized time.
public float GetNormalizedInterpolation(TSplineType spline, float t) where TSplineType : ISpline
{
return SplineUtility.GetNormalizedInterpolation(spline, t, m_IndexUnit);
}
///
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
///
/// Returns an enumerator that iterates through the DataPoints collection.
///
///
/// An IEnumerator{DataPoint{T}} for this collection.
public IEnumerator> GetEnumerator()
{
for (int i = 0, c = Count; i < c; ++i)
yield return m_DataPoints[i];
}
}
}