using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Unity.Mathematics;
using UObject = UnityEngine.Object;
namespace UnityEngine.Splines
{
///
/// The Spline class is a collection of , the closed/open state, and editing representation.
///
[Serializable]
public class Spline : ISpline, IList
{
const TangentMode k_DefaultTangentMode = TangentMode.Broken;
const BezierTangent k_DefaultMainTangent = BezierTangent.Out;
const int k_BatchModification = -1;
[Serializable]
sealed class MetaData
{
public TangentMode Mode;
public float Tension;
DistanceToInterpolation[] m_DistanceToInterpolation = new DistanceToInterpolation[k_CurveDistanceLutResolution];
public DistanceToInterpolation[] DistanceToInterpolation
{
get
{
if (m_DistanceToInterpolation == null || m_DistanceToInterpolation.Length != k_CurveDistanceLutResolution)
{
m_DistanceToInterpolation = new DistanceToInterpolation[k_CurveDistanceLutResolution];
InvalidateCache();
}
return m_DistanceToInterpolation;
}
}
float3[] m_UpVectors = new float3[k_CurveDistanceLutResolution];
public float3[] UpVectors
{
get
{
if (m_UpVectors == null || m_UpVectors.Length != k_CurveDistanceLutResolution)
{
m_UpVectors = new float3[k_CurveDistanceLutResolution];
InvalidateCache();
}
return m_UpVectors;
}
}
public MetaData()
{
Mode = k_DefaultTangentMode;
Tension = SplineUtility.CatmullRomTension;
InvalidateCache();
}
public MetaData(MetaData toCopy)
{
Mode = toCopy.Mode;
Tension = toCopy.Tension;
Array.Copy(toCopy.DistanceToInterpolation, DistanceToInterpolation, DistanceToInterpolation.Length);
Array.Copy(toCopy.UpVectors, UpVectors, UpVectors.Length);
}
public void InvalidateCache()
{
DistanceToInterpolation[0] = Splines.DistanceToInterpolation.Invalid;
UpVectors[0] = Vector3.zero;
}
}
const int k_CurveDistanceLutResolution = 30;
[SerializeField, Obsolete, HideInInspector]
#pragma warning disable CS0618
SplineType m_EditModeType = SplineType.Bezier;
#pragma warning restore CS0618
[SerializeField]
List m_Knots = new List();
float m_Length = -1f;
[SerializeField, HideInInspector]
List m_MetaData = new List();
[SerializeField]
bool m_Closed;
[SerializeField]
SplineDataDictionary m_IntData = new SplineDataDictionary();
[SerializeField]
SplineDataDictionary m_FloatData = new SplineDataDictionary();
[SerializeField]
SplineDataDictionary m_Float4Data = new SplineDataDictionary();
[SerializeField]
SplineDataDictionary m_ObjectData = new SplineDataDictionary();
IEnumerable embeddedSplineData
{
get
{
foreach (var data in m_IntData) yield return data.Value;
foreach (var data in m_FloatData) yield return data.Value;
foreach (var data in m_Float4Data) yield return data.Value;
foreach (var data in m_ObjectData) yield return data.Value;
}
}
///
/// Retrieve a reference for if it exists.
/// Note that this is a reference to the stored , not a copy. Any modifications to
/// this collection will affect the data.
///
/// The string key value to search for. Only one instance of a key value can exist in an
/// embedded collection, however keys are unique to each data type. The same key
/// can be re-used to store float data and Object data.
/// The output if the key is found.
/// True if the key and type combination are found, otherwise false.
public bool TryGetFloatData(string key, out SplineData data) => m_FloatData.TryGetValue(key, out data);
///
public bool TryGetFloat4Data(string key, out SplineData data) => m_Float4Data.TryGetValue(key, out data);
///
public bool TryGetIntData(string key, out SplineData data) => m_IntData.TryGetValue(key, out data);
///
public bool TryGetObjectData(string key, out SplineData data) => m_ObjectData.TryGetValue(key, out data);
///
/// Returns a for . If an instance matching the key and
/// type does not exist, a new entry is appended to the internal collection and returned.
/// Note that this is a reference to the stored , not a copy. Any modifications to
/// this collection will affect the data.
///
/// The string key value to search for. Only one instance of a key value can exist in an
/// embedded collection, however keys are unique to each data type. The same key
/// can be re-used to store float data and Object data.
/// A of the requested type.
public SplineData GetOrCreateFloatData(string key) => m_FloatData.GetOrCreate(key);
///
public SplineData GetOrCreateFloat4Data(string key) => m_Float4Data.GetOrCreate(key);
///
public SplineData GetOrCreateIntData(string key) => m_IntData.GetOrCreate(key);
///
public SplineData GetOrCreateObjectData(string key) => m_ObjectData.GetOrCreate(key);
///
/// Remove a value.
///
/// The string key value to search for. Only one instance of a key value can exist in an
/// embedded collection, however keys are unique to each data type. The same key
/// can be re-used to store float data and Object data.
/// Returns true if a matching key value pair was found and removed, or
/// false if no match was found.
public bool RemoveFloatData(string key) => m_FloatData.Remove(key);
///
public bool RemoveFloat4Data(string key) => m_Float4Data.Remove(key);
///
public bool RemoveIntData(string key) => m_IntData.Remove(key);
///
public bool RemoveObjectData(string key) => m_ObjectData.Remove(key);
///
/// Get a collection of the keys of embedded for this type.
///
/// An enumerable list of keys present for the requested type.
public IEnumerable GetFloatDataKeys() => m_FloatData.Keys;
///
public IEnumerable GetFloat4DataKeys() => m_Float4Data.Keys;
///
public IEnumerable GetIntDataKeys() => m_IntData.Keys;
///
public IEnumerable GetObjectDataKeys() => m_ObjectData.Keys;
///
public IEnumerable GetSplineDataKeys(EmbeddedSplineDataType type)
{
switch (type)
{
case EmbeddedSplineDataType.Float: return m_FloatData.Keys;
case EmbeddedSplineDataType.Float4: return m_Float4Data.Keys;
case EmbeddedSplineDataType.Int: return m_IntData.Keys;
case EmbeddedSplineDataType.Object: return m_ObjectData.Keys;
default: throw new InvalidEnumArgumentException();
}
}
///
/// Get a collection of the values for this type.
///
/// An enumerable list of values present for the requested type.
public IEnumerable> GetFloatDataValues() => m_FloatData.Values;
///
public IEnumerable> GetFloat4DataValues() => m_Float4Data.Values;
///
public IEnumerable> GetIntDataValues() => m_IntData.Values;
///
public IEnumerable> GetObjectDataValues() => m_ObjectData.Values;
///
/// Set the for .
///
/// The string key value to search for. Only one instance of a key value can exist in an
/// embedded collection, however keys are unique to each data type. The same key
/// can be re-used to store float data and Object data.
/// The to set. This value will be copied.
public void SetFloatData(string key, SplineData value) => m_FloatData[key] = value;
///
public void SetFloat4Data(string key, SplineData value) => m_Float4Data[key] = value;
///
public void SetIntData(string key, SplineData value) => m_IntData[key] = value;
///
public void SetObjectData(string key, SplineData value) => m_ObjectData[key] = value;
///
/// 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.
///
[Obsolete("Deprecated, use " + nameof(Changed) + " instead.")]
public event Action changed;
///
/// Invoked any time a spline is modified.
///
///
/// First parameter is the target Spline that the event is raised for, second parameter is
/// the knot index and the third parameter represents the type of change that occurred.
/// If the event does not target a specific knot, the second parameter will have the value of -1.
///
/// In the editor this callback can be invoked many times per-frame.
/// Prefer to use when
/// working with splines in the editor.
///
///
public static event Action Changed;
#if UNITY_EDITOR
internal static Action afterSplineWasModified;
[NonSerialized]
bool m_QueueAfterSplineModifiedCallback;
#endif
(float curve0, float curve1) m_LastKnotChangeCurveLengths;
internal void SetDirtyNoNotify()
{
EnsureMetaDataValid();
m_Length = -1f;
for (int i = 0, c = m_MetaData.Count; i < c; ++i)
m_MetaData[i].InvalidateCache();
}
internal void SetDirty(SplineModification modificationEvent, int knotIndex = k_BatchModification)
{
SetDirtyNoNotify();
#pragma warning disable 618
changed?.Invoke();
#pragma warning restore 618
OnSplineChanged();
foreach (var data in embeddedSplineData)
data.OnSplineModified(new SplineModificationData(this, modificationEvent, knotIndex, m_LastKnotChangeCurveLengths.curve0, m_LastKnotChangeCurveLengths.curve1));
Changed?.Invoke(this, knotIndex, modificationEvent);
#if UNITY_EDITOR
if (m_QueueAfterSplineModifiedCallback)
return;
m_QueueAfterSplineModifiedCallback = true;
UnityEditor.EditorApplication.delayCall += () =>
{
m_QueueAfterSplineModifiedCallback = false;
afterSplineWasModified?.Invoke(this);
};
#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()
{
}
void EnsureMetaDataValid()
{
while(m_MetaData.Count < m_Knots.Count)
m_MetaData.Add(new MetaData());
}
///
/// Ensure that a has the correct tangent and rotation values to match it's
/// and tension. This can be necessary if knot data is modified outside of the Spline
/// class (ex, manually setting the array without taking care to also set the tangent
/// modes).
///
/// The knot index to set tangent and rotation values for.
public void EnforceTangentModeNoNotify(int index) => EnforceTangentModeNoNotify(new SplineRange(index, 1));
///
/// Ensure that a has the correct tangent and rotation values to match it's
/// and tension. This can be necessary if knot data is modified outside of the Spline
/// class (ex, manually setting the array without taking care to also set the tangent
/// modes).
///
/// The range of knot indices to set tangent and rotation values
/// for.
public void EnforceTangentModeNoNotify(SplineRange range)
{
for(int i = range.Start; i <= range.End; ++i)
ApplyTangentModeNoNotify(i);
}
///
/// Gets the for a knot index.
///
/// The index to retrieve data for.
/// A for the knot at index.
public TangentMode GetTangentMode(int index)
{
EnsureMetaDataValid();
return m_MetaData.Count > 0 ? m_MetaData[index].Mode : k_DefaultTangentMode;
}
///
/// Sets the for all knots on this spline.
///
/// The to apply to each knot.
public void SetTangentMode(TangentMode mode)
{
SetTangentMode(new SplineRange(0, Count), mode);
}
///
/// Sets the for a knot, and ensures that the rotation and tangent values match the
/// behavior of the tangent mode.
/// This function can modify the contents of the at the specified index.
///
/// The index of the knot to set.
/// The mode to set.
/// The tangent direction to align both the In and Out tangent when assigning Continuous
/// or Mirrored tangent mode.
public void SetTangentMode(int index, TangentMode mode, BezierTangent main = k_DefaultMainTangent)
{
if (GetTangentMode(index) == mode)
return;
// In the case of an open spline, changing knot mode to a mirrored mode will change the shape of
// the spline as the considered tangent is the out tangent by default. To avoid this, we change
// the considered tangent to the in tangent when the knot is the last knot of an open spline.
if (index == Count - 1 && !Closed)
main = BezierTangent.In;
SetTangentMode(new SplineRange(index, 1), mode, main);
}
///
/// Sets the for a series of knots, and ensures that the rotation and tangent values
/// match the behavior of the tangent mode.
/// This function can modify the contents of the at the specified indices.
///
/// The range of knot indices to set.
/// The mode to set.
/// The tangent direction to align both the In and Out tangent with when Continuous or
/// Mirrored tangent mode is assigned .
public void SetTangentMode(SplineRange range, TangentMode mode, BezierTangent main = k_DefaultMainTangent)
{
foreach (var index in range)
{
CacheKnotOperationCurves(index);
SetTangentModeNoNotify(index, mode, main);
SetDirty(SplineModification.KnotModified, index);
}
}
///
/// Sets the for a knot, and ensures that the rotation and tangent values match the
/// behavior of the tangent mode. No changed callbacks will be invoked.
/// This function can modify the contents of the at the specified index.
///
/// The index of the knot to set.
/// The mode to set.
/// The tangent direction to align both the In and Out tangent when assigning Continuous
/// or Mirrored tangent mode.
public void SetTangentModeNoNotify(int index, TangentMode mode, BezierTangent main = k_DefaultMainTangent)
{
EnsureMetaDataValid();
var knot = m_Knots[index];
// If coming from a tangent mode where tangents are locked to 0 length, preset the Bezier tangents with a
// likely non-zero length.
if (m_MetaData[index].Mode == TangentMode.Linear && mode >= TangentMode.Mirrored)
{
knot.TangentIn = SplineUtility.GetExplicitLinearTangent(knot, this.Previous(index));
knot.TangentOut = SplineUtility.GetExplicitLinearTangent(knot, this.Next(index));
}
m_MetaData[index].Mode = mode;
m_Knots[index] = knot;
ApplyTangentModeNoNotify(index, main);
}
///
/// Ensures that the tangents at an index conform to the tangent mode.
///
///
/// This function updates the tangents, but does not set the tangent mode.
///
/// The index of the knot to set tangent values for.
/// The tangent direction to align the In and Out tangent to when assigning Continuous
/// or Mirrored tangent mode.
void ApplyTangentModeNoNotify(int index, BezierTangent main = k_DefaultMainTangent)
{
var knot = m_Knots[index];
var mode = GetTangentMode(index);
switch(mode)
{
case TangentMode.Continuous:
knot = knot.BakeTangentDirectionToRotation(false, main);
break;
case TangentMode.Mirrored:
knot = knot.BakeTangentDirectionToRotation(true, main);
break;
case TangentMode.Linear:
knot.TangentIn = float3.zero;
knot.TangentOut = float3.zero;
break;
case TangentMode.AutoSmooth:
knot = SplineUtility.GetAutoSmoothKnot(knot.Position,
this.Previous(index).Position,
this.Next(index).Position,
math.mul(knot.Rotation, math.up()),
m_MetaData[index].Tension);
break;
}
m_Knots[index] = knot;
SetDirtyNoNotify();
}
///
/// Gets the tension value for the requested index.
///
/// The knot index to get a tension value for.
/// Returns the tension value for the requested index.
public float GetAutoSmoothTension(int index) => m_MetaData[index].Tension;
///
/// Sets the tension that is used to calculate the magnitude of tangents when the is
/// . Valid values are between 0 and 1.
/// A lower value results in sharper curves, whereas higher values appear more rounded.
///
/// The knot index to set a tension value for.
/// Set the length of the tangent vectors.
public void SetAutoSmoothTension(int index, float tension)
{
SetAutoSmoothTension(new SplineRange(index, 1), tension);
}
///
/// Sets the tension that is used to calculate the magnitude of tangents when the is
/// . Valid values are between 0 and 1.
/// A lower value results in sharper curves, whereas higher values appear more rounded.
///
/// The range of knot indices to set a tension value for.
/// Set the length of the tangent vectors.
public void SetAutoSmoothTension(SplineRange range, float tension)
{
SetAutoSmoothTensionInternal(range, tension, true);
}
///
/// Sets the tension that is used to calculate the magnitude of tangents when the is
/// . Valid values are between 0 and 1.
/// A lower value results in sharper curves, whereas higher values appear more rounded.
/// No changed callbacks will be invoked.
///
/// The knot index to set a tension value for.
/// Set the length of the tangent vectors for a knot set to .
public void SetAutoSmoothTensionNoNotify(int index, float tension)
{
SetAutoSmoothTensionInternal(new SplineRange(index, 1), tension, false);
}
///
/// Set the tension that is used to calculate the magnitude of tangents when the is
/// . Valid values are between 0 and 1.
/// A lower value results in sharper curves, whereas higher values appear more rounded.
/// No changed callbacks will be invoked.
///
/// The range of knot indices to set a tension value for.
/// Set the length of the tangent vectors for a knot set to .
public void SetAutoSmoothTensionNoNotify(SplineRange range, float tension)
{
SetAutoSmoothTensionInternal(range, tension, false);
}
void SetAutoSmoothTensionInternal(SplineRange range, float tension, bool setDirty)
{
for (int i = 0, c = range.Count; i < c; ++i)
{
var index = range[i];
CacheKnotOperationCurves(index);
m_MetaData[index].Tension = tension;
if(m_MetaData[index].Mode == TangentMode.AutoSmooth)
ApplyTangentModeNoNotify(index);
if (setDirty)
SetDirty(SplineModification.KnotModified, index);
}
}
///
/// 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.
///
[Obsolete("Use GetTangentMode and SetTangentMode.")]
public SplineType EditType
{
get => m_EditModeType;
set
{
if (m_EditModeType == value)
return;
m_EditModeType = value;
var mode = value.GetTangentMode();
for(int i = 0; i < Count; ++i)
SetTangentModeNoNotify(i, mode);
SetDirty(SplineModification.Default);
}
}
///
/// A collection of .
///
public IEnumerable Knots
{
get => m_Knots;
set
{
m_Knots = new List(value);
m_MetaData = new List(m_Knots.Count);
SetDirty(SplineModification.Default);
}
}
///
/// 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;
CheckAutoSmoothExtremityKnots();
SetDirty(SplineModification.ClosedModified);
}
}
internal void CheckAutoSmoothExtremityKnots()
{
if (GetTangentMode(0) == TangentMode.AutoSmooth)
ApplyTangentModeNoNotify(0);
if (Count > 2 && GetTangentMode(Count - 1) == TangentMode.AutoSmooth)
ApplyTangentModeNoNotify(Count - 1);
}
///
/// 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) =>
Insert(index, knot, k_DefaultTangentMode, SplineUtility.CatmullRomTension);
///
/// Inserts a at the specified .
///
/// The zero-based index to insert the new element.
/// The to insert.
/// The to apply to this knot. Tangent modes are enforced
/// when a knot value is set.
public void Insert(int index, BezierKnot knot, TangentMode mode) =>
Insert(index, knot, mode, SplineUtility.CatmullRomTension);
///
/// Adds a at the specified .
///
/// The zero-based index to insert the new element.
/// The to insert.
/// The to apply to this knot. Tangent modes are enforced
/// when a knot value is set.
/// The modifier value that is used to calculate the magnitude of tangents when the
/// is . Valid values are between 0 and 1.
/// A lower value results in sharper curves, whereas higher values appear more rounded.
///
public void Insert(int index, BezierKnot knot, TangentMode mode, float tension)
{
CacheKnotOperationCurves(index);
InsertNoNotify(index, knot, mode, tension);
SetDirty(SplineModification.KnotInserted, index);
}
void InsertNoNotify(int index, BezierKnot knot, TangentMode mode, float tension)
{
EnsureMetaDataValid();
m_Knots.Insert(index, knot);
m_MetaData.Insert(index, new MetaData() { Mode = mode, Tension = tension });
var previousIndex = this.PreviousIndex(index);
if (previousIndex != index)
ApplyTangentModeNoNotify(previousIndex);
ApplyTangentModeNoNotify(index);
var nextIndex = this.NextIndex(index);
if (nextIndex != index)
ApplyTangentModeNoNotify(nextIndex);
}
///
/// Creates a at the specified with a normalized offset.
///
/// The zero-based index to insert the new element at.
/// The normalized offset along the curve.
///
internal void InsertOnCurve(int index, float curveT)
{
var previousIndex = SplineUtility.PreviousIndex(index, Count, Closed);
var previous = m_Knots[previousIndex];
var next = m_Knots[index];
var curveToSplit = new BezierCurve(previous, m_Knots[index]);
CurveUtility.Split(curveToSplit, curveT, out var leftCurve, out var rightCurve);
if (GetTangentMode(previousIndex) == TangentMode.Mirrored)
SetTangentMode(previousIndex, TangentMode.Continuous);
if (GetTangentMode(index) == TangentMode.Mirrored)
SetTangentMode(index, TangentMode.Continuous);
if (SplineUtility.AreTangentsModifiable(GetTangentMode(previousIndex)))
previous.TangentOut = math.mul(math.inverse(previous.Rotation), leftCurve.Tangent0);
if (SplineUtility.AreTangentsModifiable(GetTangentMode(index)))
next.TangentIn = math.mul(math.inverse(next.Rotation), rightCurve.Tangent1);
var up = CurveUtility.EvaluateUpVector(curveToSplit, curveT, math.rotate(previous.Rotation, math.up()), math.rotate(next.Rotation, math.up()));
var rotation = quaternion.LookRotationSafe(math.normalizesafe(rightCurve.Tangent0), up);
var inverseRotation = math.inverse(rotation);
SetKnotNoNotify(previousIndex, previous);
SetKnotNoNotify(index, next);
// Inserting the knot at the right position to compute correctly auto-smooth tangents
var bezierKnot = new BezierKnot(leftCurve.P3, math.mul(inverseRotation, leftCurve.Tangent1), math.mul(inverseRotation, rightCurve.Tangent0), rotation);
Insert(index, bezierKnot);
}
///
/// Removes the knot at the specified index.
///
/// The zero-based index of the element to remove.
public void RemoveAt(int index)
{
EnsureMetaDataValid();
CacheKnotOperationCurves(index);
m_Knots.RemoveAt(index);
m_MetaData.RemoveAt(index);
var next = Mathf.Clamp(index, 0, Count-1);
if (Count > 0)
{
ApplyTangentModeNoNotify(this.PreviousIndex(next));
ApplyTangentModeNoNotify(next);
}
SetDirty(SplineModification.KnotRemoved, index);
}
///
/// 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 => SetKnot(index, value);
}
///
/// Sets the value of a knot at index.
///
/// The index of the to set.
/// The to set.
/// The tangent to prioritize if the tangents are modified to conform with the
/// set for this knot.
public void SetKnot(int index, BezierKnot value, BezierTangent main = k_DefaultMainTangent)
{
CacheKnotOperationCurves(index);
SetKnotNoNotify(index, value, main);
SetDirty(SplineModification.KnotModified, index);
}
///
/// Sets the value of a knot index without invoking any change callbacks.
///
/// The index of the to set.
/// The to set.
/// The tangent to prioritize if the tangents are modified to conform with the
/// set for this knot.
public void SetKnotNoNotify(int index, BezierKnot value, BezierTangent main = k_DefaultMainTangent)
{
m_Knots[index] = value;
ApplyTangentModeNoNotify(index, main);
// setting knot position affects the tangents of neighbor auto-smooth (catmull-rom) knots
int p = this.PreviousIndex(index), n = this.NextIndex(index);
if(m_MetaData[p].Mode == TangentMode.AutoSmooth)
ApplyTangentModeNoNotify(p, main);
if(m_MetaData[n].Mode == TangentMode.AutoSmooth)
ApplyTangentModeNoNotify(n, main);
}
///
/// 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;
}
///
/// Create a spline from a collection of knot positions.
/// The knot positions are converted to .
/// When the tangent mode is set to Mirrored, Continuous, Broken, or Linear, the final tangent values are obtained from tangents initially computed using Autosmooth to ensure accuracy.
///
/// The range of knot positions to add to the spline.
/// The to apply to this range of knot positions. The default value is Autosmooth.
/// Whether the spline is open or closed. Open splines have a start and end point and closed splines form an unbroken loop.
public Spline(IEnumerable knotPositions, TangentMode tangentMode = TangentMode.AutoSmooth, bool closed = false)
{
InsertRangeNoNotify(Count, knotPositions, tangentMode);
m_Closed = closed;
}
///
/// Create a copy of a spline.
///
/// The spline to copy in that new instance.
public Spline(Spline spline)
{
m_Knots = spline.Knots.ToList();
m_Closed = spline.Closed;
//Deep copy of the 4 embedded SplineData
foreach (var data in spline.m_IntData)
m_IntData[data.Key] = data.Value;
foreach (var data in spline.m_FloatData)
m_FloatData[data.Key] = data.Value;
foreach (var data in spline.m_Float4Data)
m_Float4Data[data.Key] = data.Value;
foreach (var data in spline.m_ObjectData)
m_ObjectData[data.Key] = data.Value;
}
///
/// 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.
///
/// The knot index that serves as the first control point for this curve.
///
///
/// Returns the length of the formed by the knot at index and the next knot.
public float GetCurveLength(int index)
{
EnsureMetaDataValid();
var cumulativeCurveLengths = m_MetaData[index].DistanceToInterpolation;
if(cumulativeCurveLengths[0].Distance < 0f)
CurveUtility.CalculateCurveLengths(GetCurve(index), cumulativeCurveLengths);
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_MetaData[index].DistanceToInterpolation[0].Distance < 0f)
CurveUtility.CalculateCurveLengths(GetCurve(index), m_MetaData[index].DistanceToInterpolation);
return m_MetaData[index].DistanceToInterpolation;
}
///
/// 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);
void WarmUpCurveUps()
{
EnsureMetaDataValid();
for (int i = 0, c = Closed ? Count : Count - 1; i < c; ++i)
this.EvaluateUpVectorsForCurve(i, m_MetaData[i].UpVectors);
}
///
/// Return the up vector for a t ratio on the curve.
///
/// The index of the curve for which the length needs to be retrieved.
/// A value between 0 and 1 representing the ratio along the curve.
///
/// Returns the up vector at the t ratio of the curve of index 'index'.
///
public float3 GetCurveUpVector(int index, float t)
{
EnsureMetaDataValid();
var ups = m_MetaData[index].UpVectors;
if (math.all(ups[0] == float3.zero))
this.EvaluateUpVectorsForCurve(index, ups);
var offset = 1f / (float)(ups.Length - 1);
var curveT = 0f;
for (int i = 0; i < ups.Length; i++)
{
if (t <= curveT + offset)
return Vector3.Lerp(ups[i], ups[i + 1], (t - curveT) / offset);
curveT += offset;
}
return ups[ups.Length - 1];
}
///
/// 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();
WarmUpCurveUps();
}
///
/// Change the size of the list.
///
/// The new size of the knots collection.
public void Resize(int newSize)
{
int originalSize = Count;
newSize = math.max(0, newSize);
if (newSize == originalSize)
return;
if (newSize > originalSize)
{
while (m_Knots.Count < newSize)
Add(new BezierKnot());
}
else if (newSize < originalSize)
{
while(newSize < Count)
RemoveAt(Count-1);
var last = newSize - 1;
if(last > -1 && last < m_Knots.Count)
ApplyTangentModeNoNotify(last);
}
}
///
/// 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 copyFrom)
{
if (copyFrom == this)
return;
m_Closed = copyFrom.Closed;
m_Knots.Clear();
m_Knots.AddRange(copyFrom.m_Knots);
m_MetaData.Clear();
for (int i = 0; i < copyFrom.m_MetaData.Count; ++i)
m_MetaData.Add(new MetaData(copyFrom.m_MetaData[i]));
SetDirty(SplineModification.Default);
}
///
/// Get an enumerator that iterates through the collection.
///
/// An IEnumerator that is used to iterate the collection.
public IEnumerator GetEnumerator() => m_Knots.GetEnumerator();
///
/// Gets an enumerator that iterates through the collection.
///
/// An IEnumerator that is used to iterate the collection.
IEnumerator IEnumerable.GetEnumerator() => m_Knots.GetEnumerator();
///
/// Adds a knot to the spline.
///
/// The to add.
public void Add(BezierKnot item) => Add(item, k_DefaultTangentMode);
///
/// Creates a from a knot position and adds it to the spline.
/// When the tangent mode is set to Mirrored, Continuous, Broken, or Linear, the final tangent values are obtained from tangents initially computed using Autosmooth to ensure accuracy.
///
/// The knot position to convert to a and add to the spline.
/// The to apply to the new element. The default value is Autosmooth.
public void Add(float3 knotPosition, TangentMode tangentMode = TangentMode.AutoSmooth) =>
Insert(Count, knotPosition, tangentMode);
///
/// Creates from a range of knot positions and adds them to the spline.
/// When the tangent mode is set to Mirrored, Continuous, Broken, or Linear, the final tangent values are obtained from tangents initially computed using Autosmooth to ensure accuracy.
///
/// The range of knot positions to add to the spline.
/// The to apply to this range of knot positions. The default value is Autosmooth.
public void AddRange(IEnumerable knotPositions, TangentMode tangentMode = TangentMode.AutoSmooth) =>
InsertRange(Count, knotPositions, tangentMode);
///
/// Creates a from a knot position and inserts it at the specified .
/// When the tangent mode is set to Mirrored, Continuous, Broken, or Linear, the final tangent values are obtained from tangents initially computed using Autosmooth to ensure accuracy.
///
/// The zero-based index to insert the new element at.
/// The knot position to convert to a and insert in the spline.
/// The to apply to the new element. The default value is Autosmooth.
public void Insert(int index, float3 knotPosition, TangentMode tangentMode = TangentMode.AutoSmooth)
{
if (tangentMode == TangentMode.AutoSmooth)
{
Insert(index, new BezierKnot(knotPosition), tangentMode);
}
else
{
// Knots with a tangent mode other than Autosmooth require a two-phase insertion process.
// The first phase involves standard insertion using the Autosmooth tangent mode in order to calculate the tangents.
// The second phase involves converting the knots to the specified tangent mode.
// Tangent modes should only be set after all knots have been inserted into the spline to ensure proper computation of the tangents.
CacheKnotOperationCurves(index);
InsertNoNotify(index, new BezierKnot(knotPosition), TangentMode.AutoSmooth, SplineUtility.DefaultTension);
SetTangentModeNoNotify(index, tangentMode);
SetDirty(SplineModification.KnotInserted, index); // Ensure that KnotInserted is sent, and not KnotModified.
}
}
///
/// Creates from a range of knot positions and inserts them at the specified .
/// When the tangent mode is set to Mirrored, Continuous, Broken, or Linear, the final tangent values are obtained from tangents initially computed using Autosmooth to ensure accuracy.
///
/// The zero-based index to insert the new elements at.
/// The range of knot positions to insert in the spline.
/// The to apply to this range of knot positions. The default value is Autosmooth.
public void InsertRange(int index, IEnumerable knotPositions, TangentMode tangentMode = TangentMode.AutoSmooth)
{
InsertRangeNoNotify(index, knotPositions, tangentMode, true);
SetDirty(SplineModification.KnotInserted); // Ensure that KnotInserted is sent, and not KnotModified.
}
void InsertRangeNoNotify(int index, IEnumerable knotPositions, TangentMode tangentMode = TangentMode.AutoSmooth, bool cacheCurves = false)
{
var currentIndex = 0;
foreach (var pos in knotPositions)
{
var knotIndex = index + currentIndex;
if (cacheCurves)
CacheKnotOperationCurves(knotIndex);
InsertNoNotify(knotIndex, new BezierKnot(pos), TangentMode.AutoSmooth, SplineUtility.DefaultTension);
currentIndex++;
}
if (tangentMode != TangentMode.AutoSmooth)
{
currentIndex = 0;
// Knots with a tangent mode other than Autosmooth require a two-phase insertion process.
// The first phase involves standard insertion using the Autosmooth tangent mode in order to calculate the tangents.
// The second phase involves converting the knots to the specified tangent mode.
// Tangent modes should only be set after all knots have been inserted into the spline to ensure proper computation of the tangents.
foreach (var pos in knotPositions)
{
var knotIndex = index + currentIndex;
SetTangentModeNoNotify(knotIndex, tangentMode);
currentIndex++;
}
}
}
///
/// Adds a knot to the spline.
///
/// The to add.
/// The tangent mode for this knot.
public void Add(BezierKnot item, TangentMode mode)
{
Insert(Count, item, mode);
}
///
/// Adds a knot to the spline.
///
/// The to add.
/// The tangent mode for this knot.
/// The modifier value that is used to calculate the magnitude of tangents when the
/// is . Valid values are between 0 and 1.
/// A lower value results in sharper curves, whereas higher values appear more rounded.
///
public void Add(BezierKnot item, TangentMode mode, float tension)
{
Insert(Count, item, mode, tension);
}
///
/// Adds all knots from a given spline to this spline.
///
/// The source spline of the knots to add.
///
public void Add(Spline spline)
{
for (int i = 0; i < spline.Count; ++i)
Insert(Count, spline[i], spline.GetTangentMode(i), spline.GetAutoSmoothTension(i));
}
///
/// Remove all knots from the spline.
///
public void Clear()
{
m_Knots.Clear();
m_MetaData.Clear();
SetDirty(SplineModification.KnotRemoved);
}
///
/// 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)
{
var index = m_Knots.IndexOf(item);
if (index >= 0)
{
RemoveAt(index);
return true;
}
return false;
}
///
/// Remove any unused embedded entries.
///
///
///
///
///
internal void RemoveUnusedSplineData()
{
m_FloatData.RemoveEmpty();
m_Float4Data.RemoveEmpty();
m_IntData.RemoveEmpty();
m_ObjectData.RemoveEmpty();
}
internal void CacheKnotOperationCurves(int index)
{
if (Count <= 1)
return;
m_LastKnotChangeCurveLengths.curve0 = GetCurveLength(this.PreviousIndex(index));
if (index < Count)
m_LastKnotChangeCurveLengths.curve1 = GetCurveLength(index);
}
}
}