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