using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Collections;
using Unity.Mathematics;
namespace UnityEngine.Splines
{
///
/// A collection of methods for extracting information about types.
///
///
/// `SplineUtility` methods do not consider Transform values except where explicitly requested. To perform operations in world space, you can use the evaluate methods or build a with a constructor that accepts a matrix and evaluate that spline.
///
public static class SplineUtility
{
const int k_SubdivisionCountMin = 6;
const int k_SubdivisionCountMax = 1024;
///
/// The default tension value used for knots.
/// Use with and
/// to control the curvature of the spline at control
/// points.
///
public const float DefaultTension = 1 / 3f;
///
/// The tension value for a Catmull-Rom type spline.
/// Use with and
/// to control the curvature of the spline at control
/// points.
///
//todo Deprecate in 3.0.
public const float CatmullRomTension = 1 / 2f;
///
/// The minimum resolution allowable when unrolling a curve to hit test while picking (selecting a spline with a cursor).
///
/// Pick resolution is used when determining how many segments are required to unroll a curve. Unrolling is the
/// process of calculating a series of line segments to approximate a curve. Some functions in SplineUtility
/// allow you to specify a resolution. Lower resolution means fewer segments, while higher resolutions result
/// in more segments. Use lower resolutions where performance is critical and accuracy is not paramount. Use
/// higher resolution where a fine degree of accuracy is necessary and performance is less important.
///
public const int PickResolutionMin = 2;
///
/// The default resolution used when unrolling a curve to hit test while picking (selecting a spline with a cursor).
///
/// Pick resolution is used when determining how many segments are required to unroll a curve. Unrolling is the
/// process of calculating a series of line segments to approximate a curve. Some functions in SplineUtility
/// allow you to specify a resolution. Lower resolution means fewer segments, while higher resolutions result
/// in more segments. Use lower resolutions where performance is critical and accuracy is not paramount. Use
/// higher resolution where a fine degree of accuracy is necessary and performance is less important.
///
public const int PickResolutionDefault = 4;
///
/// The maximum resolution allowed when unrolling a curve to hit test while picking (selecting a spline with a cursor).
///
/// Pick resolution is used when determining how many segments are required to unroll a curve. Unrolling is the
/// process of calculating a series of line segments to approximate a curve. Some functions in SplineUtility
/// allow you to specify a resolution. Lower resolution means fewer segments, while higher resolutions result
/// in more segments. Use lower resolutions where performance is critical and accuracy is not paramount. Use
/// higher resolution where a fine degree of accuracy is necessary and performance is less important.
///
public const int PickResolutionMax = 64;
///
/// The default resolution used when unrolling a curve to draw a preview in the Scene View.
///
/// Pick resolution is used when determining how many segments are required to unroll a curve. Unrolling is the
/// process of calculating a series of line segments to approximate a curve. Some functions in SplineUtility
/// allow you to specify a resolution. Lower resolution means fewer segments, while higher resolutions result
/// in more segments. Use lower resolutions where performance is critical and accuracy is not paramount. Use
/// higher resolution where a fine degree of accuracy is necessary and performance is less important.
///
public const int DrawResolutionDefault = 10;
///
/// Compute interpolated position, direction and upDirection at ratio t. Calling this method to get the
/// 3 vectors is faster than calling independently EvaluatePosition, EvaluateDirection and EvaluateUpVector
/// for the same time t as it reduces some redundant computation.
///
/// The spline to interpolate.
/// A value between 0 and 1 representing the ratio along the curve.
/// Output variable for the float3 position at t.
/// Output variable for the float3 tangent at t.
/// Output variable for the float3 up direction at t.
/// A type implementing ISpline.
/// True if successful.
public static bool Evaluate(this T spline,
float t,
out float3 position,
out float3 tangent,
out float3 upVector
) where T : ISpline
{
if (spline.Count < 1)
{
position = float3.zero;
tangent = new float3(0, 0, 1);
upVector = new float3(0, 1, 0);
return false;
}
var curveIndex = SplineToCurveT(spline, t, out var curveT);
var curve = spline.GetCurve(curveIndex);
position = CurveUtility.EvaluatePosition(curve, curveT);
tangent = CurveUtility.EvaluateTangent(curve, curveT);
upVector = spline.GetCurveUpVector(curveIndex, curveT);
return true;
}
///
/// Computes the interpolated position for NURBS defined by order, controlPoints, and knotVector at ratio t.
///
/// The value between knotVector[0] and knotVector[-1] that represents the ratio along the curve.
/// The control points for the NURBS.
/// The knot vector for the NURBS. There must be at least order + controlPoints.Length - 1 knots.
/// The order of the curve. For example, 4 for a cubic curve or 3 for quadratic.
/// The output variable for the float3 position at t.
/// True if successful.
public static bool EvaluateNurbs(
float t,
List controlPoints,
List knotVector,
int order,
out float3 position
)
{
position = float3.zero;
if (knotVector.Count < controlPoints.Count + order - 1 || controlPoints.Count < order || t < 0 || 1 < t)
{
return false;
}
knotVector = new List(knotVector);
var originalFirstKnot = knotVector[0];
var fullKnotSpan = knotVector[knotVector.Count - 1] - knotVector[0];
//normalize knots
if (knotVector[0] != 0 || knotVector[knotVector.Count - 1] != 1)
{
for (int i = 0; i < knotVector.Count; ++i)
{
knotVector[i] = (knotVector[i] - originalFirstKnot) / fullKnotSpan;
}
}
var span = order;
while (span < controlPoints.Count && knotVector[span] <= t)
{
span++;
}
span--;
var basis = SplineUtility.GetNurbsBasisFunctions(order, t, knotVector, span);
for (int i = 0; i < order; ++i)
{
position += basis[i] * controlPoints[span - order + 1 + i];
}
return true;
}
static float[] GetNurbsBasisFunctions(int degree, float t, List knotVector, int span)
{
//Constructs the Basis functions at t for the nurbs curve.
//The nurbs basis function form can be found at this link under the section
//"Construction of the basis functions": https://en.wikipedia.org/wiki/Non-uniform_rational_B-spline
//This is an iterative way of computing the same thing.
var left = new float[degree];
var right = new float[degree];
var N = new float[degree];
for (int j = 0; j < degree; ++j)
{
left[j] = 0f;
right[j] = 0f;
N[j] = 1f;
}
for (int j = 1; j < degree; ++j)
{
left[j] = (float)(t - knotVector[span + 1 - j]);
right[j] = (float)(knotVector[span + j] - t);
var saved = 0f;
for (int k = 0; k < j; k++)
{
float temp = N[k] / (right[k + 1] + left[j - k]);
N[k] = saved + right[k + 1] * temp;
saved = left[j - k] * temp;
}
N[j] = saved;
}
return N;
}
///
/// Return an interpolated position at ratio t.
///
/// The spline to interpolate.
/// A value between 0 and 1 representing the ratio along the curve.
/// A type implementing ISpline.
/// A position on the spline.
public static float3 EvaluatePosition(this T spline, float t) where T : ISpline
{
if (spline.Count < 1)
return float.PositiveInfinity;
var curve = spline.GetCurve(SplineToCurveT(spline, t, out var curveT));
return CurveUtility.EvaluatePosition(curve, curveT);
}
///
/// Return an interpolated direction at ratio t.
///
/// The spline to interpolate.
/// A value between 0 and 1 representing the ratio along the curve.
/// A type implementing ISpline.
/// A direction on the spline.
public static float3 EvaluateTangent(this T spline, float t) where T : ISpline
{
if (spline.Count < 1)
return float.PositiveInfinity;
var curve = spline.GetCurve(SplineToCurveT(spline, t, out var curveT));
return CurveUtility.EvaluateTangent(curve, curveT);
}
///
/// Evaluate the normal (up) vector of a spline.
///
/// The spline to evaluate.
/// A value between 0 and 1 representing a ratio of the curve.
/// A type implementing ISpline.
/// An up vector
public static float3 EvaluateUpVector(this T spline, float t) where T : ISpline
{
if (spline.Count < 1)
return float3.zero;
var curveIndex = SplineToCurveT(spline, t, out var curveT);
return spline.GetCurveUpVector(curveIndex, curveT);
}
///
/// Calculate the normal (up) vector of a spline. This is a more accurate but more expensive operation
/// than .
///
/// The to evaluate.
/// A value between 0 and 1 representing a ratio of the curve.
/// A type implementing ISpline.
/// An up vector
public static float3 CalculateUpVector(this T spline, float t) where T : ISpline
{
if (spline.Count < 1)
return float3.zero;
var curveIndex = SplineToCurveT(spline, t, out var curveT);
return spline.CalculateUpVector(curveIndex, curveT);
}
internal static float3 CalculateUpVector(this T spline, int curveIndex, float curveT) where T : ISpline
{
if (spline.Count < 1)
return float3.zero;
var curve = spline.GetCurve(curveIndex);
var curveStartRotation = spline[curveIndex].Rotation;
var curveStartUp = math.rotate(curveStartRotation, math.up());
if (curveT == 0f)
return curveStartUp;
var endKnotIndex = spline.NextIndex(curveIndex);
var curveEndRotation = spline[endKnotIndex].Rotation;
var curveEndUp = math.rotate(curveEndRotation, math.up());
if (curveT == 1f)
return curveEndUp;
var up = CurveUtility.EvaluateUpVector(curve, curveT, curveStartUp, curveEndUp);
return up;
}
internal static void EvaluateUpVectorsForCurve(this T spline, int curveIndex, NativeArray upVectors)
where T : ISpline
{
if (spline.Count < 1 || !upVectors.IsCreated)
return;
var curveStartRotation = spline[curveIndex].Rotation;
var curveStartUp = math.rotate(curveStartRotation, math.up());
var endKnotIndex = spline.NextIndex(curveIndex);
var curveEndRotation = spline[endKnotIndex].Rotation;
var curveEndUp = math.rotate(curveEndRotation, math.up());
CurveUtility.EvaluateUpVectors(spline.GetCurve(curveIndex), curveStartUp, curveEndUp, upVectors);
}
internal static void EvaluateUpVectorsForCurve(this T spline, int curveIndex, float3[] upVectors) where T : ISpline
{
if (upVectors == null)
return;
var upVectorsNative = new NativeArray(upVectors, Allocator.Temp);
EvaluateUpVectorsForCurve(spline, curveIndex, upVectorsNative);
upVectorsNative.CopyTo(upVectors);
}
///
/// Return an interpolated acceleration at ratio t.
///
/// The spline to interpolate.
/// A type implementing ISpline.
/// A value between 0 and 1 representing the ratio along the curve.
/// An acceleration on the spline.
public static float3 EvaluateAcceleration(this T spline, float t) where T : ISpline
{
if (spline.Count < 1)
return float3.zero;
var curve = spline.GetCurve(SplineToCurveT(spline, t, out var curveT));
return CurveUtility.EvaluateAcceleration(curve, curveT);
}
///
/// Return an interpolated curvature at ratio t.
///
/// The spline to interpolate.
/// A type implementing ISpline.
/// A value between 0 and 1 representing the ratio along the curve.
/// A curvature on the spline.
public static float EvaluateCurvature(this T spline, float t) where T : ISpline
{
if (spline.Count < 1)
return 0f;
var curveIndex = SplineToCurveT(spline, t, out var curveT);
var curve = spline.GetCurve(curveIndex);
return CurveUtility.EvaluateCurvature(curve, curveT);
}
///
/// Return the curvature center at ratio t. The curvature center represents the center of the circle
/// that is tangent to the curve at t. This circle is in the plane defined by the curve velocity (tangent)
/// and the curve acceleration at that point.
///
/// The spline to interpolate.
/// A type implementing ISpline.
/// A value between 0 and 1 representing the ratio along the curve.
/// A point representing the curvature center associated to the position at t on the spline.
public static float3 EvaluateCurvatureCenter(this T spline, float t) where T : ISpline
{
if (spline.Count < 1)
return 0f;
var curveIndex = SplineToCurveT(spline, t, out var curveT);
var curve = spline.GetCurve(curveIndex);
var curvature = CurveUtility.EvaluateCurvature(curve, curveT);
if (curvature != 0)
{
var radius = 1f / curvature;
var position = CurveUtility.EvaluatePosition(curve, curveT);
var velocity = CurveUtility.EvaluateTangent(curve, curveT);
var acceleration = CurveUtility.EvaluateAcceleration(curve, curveT);
var curvatureUp = math.normalize(math.cross(acceleration, velocity));
var curvatureRight = math.normalize(math.cross(velocity, curvatureUp));
return position + radius * curvatureRight;
}
return float3.zero;
}
///
/// Given a normalized interpolation (t) for a spline, calculate the curve index and curve-relative
/// normalized interpolation.
///
/// The target spline.
/// A normalized spline interpolation value to be converted into curve space.
/// A normalized curve interpolation value.
/// A type implementing ISpline.
/// The curve index.
public static int SplineToCurveT(this T spline, float splineT, out float curveT) where T : ISpline
{
return SplineToCurveT(spline, splineT, out curveT, true);
}
static int SplineToCurveT(this T spline, float splineT, out float curveT, bool useLUT) where T : ISpline
{
var knotCount = spline.Count;
if (knotCount <= 1)
{
curveT = 0f;
return 0;
}
splineT = math.clamp(splineT, 0, 1);
var tLength = splineT * spline.GetLength();
var start = 0f;
var closed = spline.Closed;
for (int i = 0, c = closed ? knotCount : knotCount - 1; i < c; i++)
{
var index = i % knotCount;
var curveLength = spline.GetCurveLength(index);
if (tLength <= (start + curveLength))
{
curveT = useLUT ?
spline.GetCurveInterpolation(index, tLength - start) :
(tLength - start) / curveLength;
return index;
}
start += curveLength;
}
curveT = 1f;
return closed ? knotCount - 1 : knotCount - 2;
}
///
/// Given an interpolation value for a curve, calculate the relative normalized spline interpolation.
///
/// The target spline.
/// A curve index and normalized interpolation. The curve index is represented by the
/// integer part of the float, and interpolation is the fractional part. This is the format used by
/// .
///
/// A type implementing ISpline.
/// An interpolation value relative to normalized Spline length (0 to 1).
///
public static float CurveToSplineT(this T spline, float curve) where T : ISpline
{
// Clamp negative curve index to 0
if (spline.Count <= 1 || curve < 0f)
return 0f;
// Clamp postive curve index beyond last knot to 1
if (curve >= (spline.Closed ? spline.Count : spline.Count - 1))
return 1f;
var curveIndex = (int)math.floor(curve);
float t = 0f;
for (int i = 0; i < curveIndex; i++)
t += spline.GetCurveLength(i);
t += spline.GetCurveLength(curveIndex) * math.frac(curve);
return t / spline.GetLength();
}
///
/// Calculate the length of a spline when transformed by a matrix.
///
/// The spline whose length is to be calculated.
/// A type implementing ISpline.
/// The transformation matrix to apply to the spline.
/// The length of the transformed spline.
public static float CalculateLength(this T spline, float4x4 transform) where T : ISpline
{
using var nativeSpline = new NativeSpline(spline, transform);
return nativeSpline.GetLength();
}
///
/// Calculates the number of curves in a spline.
///
/// A type implementing ISpline.
/// The spline for which to calculate the number of curves.
/// The number of curves in a spline.
public static int GetCurveCount(this T spline) where T : ISpline
{
return math.max(0, spline.Count - (spline.Closed ? 0 : 1));
}
///
/// Calculate the bounding box of a Spline.
///
/// The spline for which to calculate bounds.
/// A type implementing ISpline.
/// The bounds of a spline.
public static Bounds GetBounds(this T spline) where T : ISpline
{
return GetBounds(spline, float4x4.identity);
}
///
/// Creates a bounding box for a spline.
///
/// The spline to calculate bounds for.
/// The matrix to transform the spline's elements with.
/// A type implementing ISpline.
/// The bounds of a spline.
public static Bounds GetBounds(this T spline, float4x4 transform) where T : ISpline
{
if (spline.Count < 1)
return default;
var knot = spline[0];
Bounds bounds = new Bounds(math.transform(transform, knot.Position), Vector3.zero);
// Only encapsulate first tangentIn if the spline is closed - otherwise it's not contributing to the spline's shape.
if (spline.Closed)
bounds.Encapsulate(math.transform(transform, knot.Position + math.rotate(knot.Rotation, knot.TangentIn)));
bounds.Encapsulate(math.transform(transform, knot.Position + math.rotate(knot.Rotation, knot.TangentOut)));
for (int i = 1, c = spline.Count; i < c; ++i)
{
knot = spline[i];
bounds.Encapsulate(math.transform(transform, knot.Position));
bounds.Encapsulate(math.transform(transform, knot.Position + math.rotate(knot.Rotation, knot.TangentIn)));
// Encapsulate last tangentOut if the spline is closed - otherwise it's not contributing to the spline's shape.
if (spline.Closed || (!spline.Closed && i < c - 1))
bounds.Encapsulate(math.transform(transform, knot.Position + math.rotate(knot.Rotation, knot.TangentOut)));
}
return bounds;
}
///
/// Gets the number of segments for a specified spline length and resolution.
///
/// The length of the spline to consider.
/// The value used to calculate the number of segments for a length. This is calculated
/// as max(MIN_SEGMENTS, min(MAX_SEGMENTS, sqrt(length) * resolution)).
///
///
/// The number of segments for a length and resolution.
///
[Obsolete("Use " + nameof(GetSubdivisionCount) + " instead.", false)]
public static int GetSegmentCount(float length, int resolution) => GetSubdivisionCount(length, resolution);
///
/// Gets the number of subdivisions for a spline length and resolution.
///
/// The length of the spline to consider.
/// The resolution to consider. Higher resolutions result in more
/// precise representations. However, higher resolutions have higher performance requirements.
///
///
/// The number of subdivisions as calculated for given length and resolution.
///
public static int GetSubdivisionCount(float length, int resolution)
{
return (int)math.max(k_SubdivisionCountMin, math.min(k_SubdivisionCountMax, math.sqrt(length) * resolution));
}
struct Segment
{
public float start, length;
public Segment(float start, float length)
{
this.start = start;
this.length = length;
}
}
static Segment GetNearestPoint(T spline,
float3 ro, float3 rd,
Segment range,
out float distance, out float3 nearest, out float time,
int segments) where T : ISpline
{
distance = float.PositiveInfinity;
nearest = float.PositiveInfinity;
time = float.PositiveInfinity;
Segment segment = new Segment(-1f, 0f);
float t0 = range.start;
float3 a = EvaluatePosition(spline, t0);
for (int i = 1; i < segments; i++)
{
float t1 = range.start + (range.length * (i / (segments - 1f)));
float3 b = EvaluatePosition(spline, t1);
var (rayPoint, linePoint) = SplineMath.RayLineNearestPoint(ro, rd, a, b, out _, out var lineParam);
float dsqr = math.lengthsq(linePoint - rayPoint);
if (dsqr < distance)
{
segment.start = t0;
segment.length = t1 - t0;
time = segment.start + segment.length * lineParam;
distance = dsqr;
nearest = linePoint;
}
t0 = t1;
a = b;
}
distance = math.sqrt(distance);
return segment;
}
static Segment GetNearestPoint(T spline,
float3 point,
Segment range,
out float distance, out float3 nearest, out float time,
int segments) where T : ISpline
{
distance = float.PositiveInfinity;
nearest = float.PositiveInfinity;
time = float.PositiveInfinity;
Segment segment = new Segment(-1f, 0f);
float t0 = range.start;
float3 a = EvaluatePosition(spline, t0);
for (int i = 1; i < segments; i++)
{
float t1 = range.start + (range.length * (i / (segments - 1f)));
float3 b = EvaluatePosition(spline, t1);
var p = SplineMath.PointLineNearestPoint(point, a, b, out var lineParam);
float dsqr = math.distancesq(p, point);
if (dsqr < distance)
{
segment.start = t0;
segment.length = t1 - t0;
time = segment.start + segment.length * lineParam;
distance = dsqr;
nearest = p;
}
t0 = t1;
a = b;
}
distance = math.sqrt(distance);
return segment;
}
///
/// Calculate the point on a spline nearest to a ray.
///
/// The input spline to search for nearest point.
/// The input ray to search against.
/// The point on a spline nearest to the input ray. The accuracy of this value is
/// affected by the .
/// A type implementing ISpline.
/// The normalized time value to the nearest point.
/// Affects how many segments to split a spline into when calculating the nearest point.
/// Higher values mean smaller and more segments, which increases accuracy at the cost of processing time.
/// The minimum resolution is defined by , and the maximum is defined by
/// .
/// In most cases, the default resolution is appropriate. Use with to fine tune
/// point accuracy.
///
///
/// The nearest point is calculated by finding the nearest point on the entire length
/// of the spline using to divide into equally spaced line segments. Successive
/// iterations will then subdivide further the nearest segment, producing more accurate results. In most cases,
/// the default value is sufficient, but if extreme accuracy is required this value can be increased to a
/// maximum of .
///
/// The distance from ray to nearest point.
public static float GetNearestPoint(T spline,
Ray ray,
out float3 nearest,
out float t,
int resolution = PickResolutionDefault,
int iterations = 2) where T : ISpline
{
float distance = float.PositiveInfinity;
nearest = float.PositiveInfinity;
float3 ro = ray.origin, rd = ray.direction;
Segment segment = new Segment(0f, 1f);
t = 0f;
int res = math.min(math.max(PickResolutionMin, resolution), PickResolutionMax);
for (int i = 0, c = math.min(10, iterations); i < c; i++)
{
int segments = GetSubdivisionCount(spline.GetLength() * segment.length, res);
segment = GetNearestPoint(spline, ro, rd, segment, out distance, out nearest, out t, segments);
}
return distance;
}
///
/// Calculate the point on a spline nearest to a point.
///
/// The input spline to search for nearest point.
/// The input point to compare.
/// The point on a spline nearest to the input point. The accuracy of this value is
/// affected by the .
/// The normalized interpolation ratio corresponding to the nearest point.
/// Affects how many segments to split a spline into when calculating the nearest point.
/// Higher values mean smaller and more segments, which increases accuracy at the cost of processing time.
/// The minimum resolution is defined by , and the maximum is defined by
/// .
/// In most cases, the default resolution is appropriate. Use with to fine tune
/// point accuracy.
///
///
/// The nearest point is calculated by finding the nearest point on the entire length
/// of the spline using to divide into equally spaced line segments. Successive
/// iterations will then subdivide further the nearest segment, producing more accurate results. In most cases,
/// the default value is sufficient, but if extreme accuracy is required this value can be increased to a
/// maximum of .
///
/// A type implementing ISpline.
/// The distance from input point to nearest point on spline.
public static float GetNearestPoint(T spline,
float3 point,
out float3 nearest,
out float t,
int resolution = PickResolutionDefault,
int iterations = 2) where T : ISpline
{
float distance = float.PositiveInfinity;
nearest = float.PositiveInfinity;
Segment segment = new Segment(0f, 1f);
t = 0f;
int res = math.min(math.max(PickResolutionMin, resolution), PickResolutionMax);
for (int i = 0, c = math.min(10, iterations); i < c; i++)
{
int segments = GetSubdivisionCount(spline.GetLength() * segment.length, res);
segment = GetNearestPoint(spline, point, segment, out distance, out nearest, out t, segments);
}
return distance;
}
///
/// Given a Spline and interpolation ratio, calculate the 3d point at a linear distance from point at spline.EvaluatePosition(t).
/// Returns the corresponding time associated to this 3d position on the Spline.
///
/// The Spline on which to compute the point.
/// A type implementing ISpline.
/// The Spline interpolation ratio `t` (normalized) from which the next position need to be computed.
///
/// The relative distance at which the new point should be placed. A negative value will compute a point at a
/// `resultPointTime` previous to `fromT` (backward search).
///
/// The normalized interpolation ratio of the resulting point.
/// The 3d point from the spline located at a linear distance from the point at t.
public static float3 GetPointAtLinearDistance(this T spline,
float fromT,
float relativeDistance,
out float resultPointT) where T : ISpline
{
const float epsilon = 0.001f;
if (fromT < 0)
{
resultPointT = 0f;
return spline.EvaluatePosition(0f);
}
var length = spline.GetLength();
var lengthAtT = fromT * length;
float currentLength = lengthAtT;
if (currentLength + relativeDistance >= length) //relativeDistance >= 0 -> Forward search
{
resultPointT = 1f;
return spline.EvaluatePosition(1f);
}
else if (currentLength + relativeDistance <= 0) //relativeDistance < 0 -> Forward search
{
resultPointT = 0f;
return spline.EvaluatePosition(0f);
}
var currentPos = spline.EvaluatePosition(fromT);
resultPointT = fromT;
var forwardSearch = relativeDistance >= 0;
var residual = math.abs(relativeDistance);
float linearDistance = 0;
float3 point = currentPos;
while (residual > epsilon && (forwardSearch ? resultPointT < 1f : resultPointT > 0))
{
currentLength += forwardSearch ? residual : -residual;
resultPointT = currentLength / length;
if (resultPointT > 1f) //forward search
resultPointT = 1f;
else if (resultPointT < 0f) //backward search
resultPointT = 0f;
point = spline.EvaluatePosition(resultPointT);
linearDistance = math.distance(currentPos, point);
residual = math.abs(relativeDistance) - linearDistance;
}
return point;
}
///
/// Given a normalized interpolation ratio, calculate the associated interpolation value in another regarding a specific spline.
///
/// The Spline to use for the conversion.
/// Normalized interpolation ratio (0 to 1).
/// The to which `t` should be converted.
/// A type implementing .
/// The interpolation value converted to targetPathUnit.
public static float ConvertIndexUnit(this T spline, float t, PathIndexUnit targetPathUnit)
where T : ISpline
{
if (targetPathUnit == PathIndexUnit.Normalized)
return WrapInterpolation(t, spline.Closed);
return ConvertNormalizedIndexUnit(spline, t, targetPathUnit);
}
///
/// Given an interpolation value using one of the various types, calculate the associated interpolation value in another regarding a specific spline.
///
/// The spline to use for the conversion.
/// Interpolation value in the original `fromPathUnit`.
/// The for the original interpolation value type.
/// The to which `value` should be converted.
/// A type implementing .
/// The interpolation value converted to targetPathUnit.
public static float ConvertIndexUnit(this T spline, float value, PathIndexUnit fromPathUnit, PathIndexUnit targetPathUnit)
where T : ISpline
{
if (fromPathUnit == targetPathUnit)
{
if (targetPathUnit == PathIndexUnit.Normalized)
value = WrapInterpolation(value, spline.Closed);
return value;
}
return ConvertNormalizedIndexUnit(spline, GetNormalizedInterpolation(spline, value, fromPathUnit), targetPathUnit);
}
static float ConvertNormalizedIndexUnit(T spline, float t, PathIndexUnit targetPathUnit) where T : ISpline
{
switch (targetPathUnit)
{
case PathIndexUnit.Knot:
//LUT SHOULD NOT be used here as PathIndexUnit.KnotIndex is linear regarding the distance
//(and thus not be interpreted using the LUT and the interpolated T)
int splineIndex = spline.SplineToCurveT(t, out float curveTime, false);
return splineIndex + curveTime;
case PathIndexUnit.Distance:
return t * spline.GetLength();
default:
return t;
}
}
static float WrapInterpolation(float t, bool closed)
{
if (!closed)
return math.clamp(t, 0f, 1f);
return t % 1f == 0f ? math.clamp(t, 0f, 1f) : t - math.floor(t);
}
///
/// Given an interpolation value in any PathIndexUnit type, calculate the normalized interpolation ratio value
/// relative to a .
///
/// The Spline to use for the conversion, this is necessary to compute Normalized and Distance PathIndexUnits.
/// The `t` value to normalize in the original PathIndexUnit.
/// The PathIndexUnit from the original `t`.
/// A type implementing ISpline.
/// The normalized interpolation ratio (0 to 1).
public static float GetNormalizedInterpolation(T spline, float t, PathIndexUnit originalPathUnit) where T : ISpline
{
switch (originalPathUnit)
{
case PathIndexUnit.Knot:
return WrapInterpolation(CurveToSplineT(spline, t), spline.Closed);
case PathIndexUnit.Distance:
var length = spline.GetLength();
return WrapInterpolation(length > 0 ? t / length : 0f, spline.Closed);
default:
return WrapInterpolation(t, spline.Closed);
}
}
///
/// Gets the index of a knot that precedes a spline index. This method uses the
/// and properties to ensure that it returns the correct index of the knot.
///
/// The spline to consider.
/// The current index to consider.
/// A type that implements ISpline.
/// Returns a knot index that precedes the `index` on the considered spline.
public static int PreviousIndex(this T spline, int index) where T : ISpline
=> PreviousIndex(index, spline.Count, spline.Closed);
///
/// Gets the index of a knot that follows a spline index. This method uses the and
/// properties to ensure that it returns the correct index of the knot.
///
/// The spline to consider.
/// The current index to consider.
/// A type that implements ISpline.
/// The knot index after `index` on the considered spline.
public static int NextIndex(this T spline, int index) where T : ISpline
=> NextIndex(index, spline.Count, spline.Closed);
///
/// Gets the before spline[index]. This method uses the
/// and properties to ensure that it returns the correct knot.
///
/// The spline to consider.
/// The current index to consider.
/// A type that implements ISpline.
/// The knot before the knot at spline[index].
public static BezierKnot Previous(this T spline, int index) where T : ISpline
=> spline[PreviousIndex(spline, index)];
///
/// Gets the after spline[index]. This method uses the
/// and properties to ensure that it returns the correct knot.
///
/// The spline to consider.
/// The current index to consider.
/// A type that implements ISpline.
/// The knot after the knot at spline[index].
public static BezierKnot Next(this T spline, int index) where T : ISpline
=> spline[NextIndex(spline, index)];
internal static int PreviousIndex(int index, int count, bool wrap)
{
return wrap ? (index + (count - 1)) % count : math.max(index - 1, 0);
}
internal static int NextIndex(int index, int count, bool wrap)
{
return wrap ? (index + 1) % count : math.min(index + 1, count - 1);
}
internal static float3 GetExplicitLinearTangent(float3 point, float3 to)
{
return (to - point) / 3.0f;
}
// Typically a linear tangent is stored as a zero length vector. When a tangent is user controlled, we need to
// extrude out the tangent to something a user can grab rather than leaving the tangent on top of the knot.
internal static float3 GetExplicitLinearTangent(BezierKnot from, BezierKnot to)
{
var tin = to.Position - from.Position;
return math.mul(math.inverse(from.Rotation), tin * .33f);
}
///
/// Calculates a tangent from the previous and next knot positions.
///
/// The position of the previous .
/// The position of the next .
/// Returns a tangent calculated from the previous and next knot positions.
// todo Deprecate in 3.0 - this is not a correct uniform catmull rom parameterization.
public static float3 GetCatmullRomTangent(float3 previous, float3 next) =>
GetAutoSmoothTangent(previous, next, CatmullRomTension);
///
/// Calculates a tangent from the previous and next knot positions.
///
/// The position of the previous .
/// The position of the next .
/// Set the length of the tangent vectors.
/// Returns a tangent calculated from the previous and next knot positions.
public static float3 GetAutoSmoothTangent(float3 previous, float3 next, float tension = DefaultTension)
{
if (next.Equals(previous))
return 0f;
return (next - previous) / math.sqrt(math.length(next - previous)) * tension;
}
///
/// Gets a tangent from the previous, current, and next knot positions.
///
/// The position of the previous .
/// The position of the current .
/// The position of the next .
/// The length of the tangent vectors.
/// Returns a tangent calculated from the previous, current, and next knot positions.
public static float3 GetAutoSmoothTangent(float3 previous, float3 current, float3 next, float tension = DefaultTension)
{
var d1 = math.length(current - previous);
var d2 = math.length(next - current);
if (d1 == 0f) // If we're only working with 2 valid points, then calculate roughly Uniform parametrization tangent and scale it down so we can atleast build rotation.
return (next - current) * 0.1f;
else if (d2 == 0f)
return (current - previous) * 0.1f;
// Calculations below are based on (pp. 5-6): http://www.cemyuksel.com/research/catmullrom_param/catmullrom_cad.pdf
// Catmull-Rom parameterization: a = 0 - Uniform, a = 0.5 - Centripetal, a = 1.0 - Chordal.
var a = tension;
var twoA = 2f * tension;
var d1PowA = math.pow(d1, a);
var d1Pow2A = math.pow(d1, twoA);
var d2PowA = math.pow(d2, a);
var d2Pow2A = math.pow(d2, twoA);
return (d1Pow2A * next - d2Pow2A * previous + (d2Pow2A - d1Pow2A) * current) / (3f * d1PowA * (d1PowA + d2PowA));
}
static float3 GetUniformAutoSmoothTangent(float3 previous, float3 next, float tension)
{
return (next - previous) * tension;
}
///
/// Gets a with its tangents and rotation calculated using the previous and next knot positions.
///
/// The position of the knot.
/// The knot that immediately precedes the requested knot.
/// The knot that immediately follows the requested knot.
/// A with tangent and rotation values calculated from the previous and next knot positions.
public static BezierKnot GetAutoSmoothKnot(float3 position, float3 previous, float3 next) =>
GetAutoSmoothKnot(position, previous, next, math.up());
///
/// Gets a with its tangents and rotation calculated using the previous and next knot positions.
///
/// The position of the knot.
/// The knot that immediately precedes the requested knot.
/// The knot that immediately follows the requested knot.
/// The normal vector of the knot.
/// A with tangent and rotation values calculated from the previous and next knot positions.
public static BezierKnot GetAutoSmoothKnot(float3 position, float3 previous, float3 next, float3 normal) =>
GetAutoSmoothKnot(position, previous, next, normal, CatmullRomTension);
///
/// Gets a with its tangents and rotation calculated using the previous and next knot positions.
///
/// The position of the knot.
/// The knot that immediately precedes the requested knot.
/// The knot that immediately follows the requested knot.
/// The normal vector of the knot.
/// Set the length of the tangent vectors.
/// A with tangent and rotation values calculated from the previous and next knot positions.
public static BezierKnot GetAutoSmoothKnot(float3 position, float3 previous, float3 next, float3 normal, float tension = DefaultTension)
{
var tanIn = GetAutoSmoothTangent(next, position, previous, tension);
var tanOut = GetAutoSmoothTangent(previous, position, next, tension);
var dirIn = new float3(0, 0, math.length(tanIn));
var dirOut = new float3(0, 0, math.length(tanOut));
var dirRot = tanOut;
if (dirIn.z == 0f)
dirIn.z = dirOut.z;
if (dirOut.z == 0f)
{
dirOut.z = dirIn.z;
dirRot = -tanIn;
}
return new BezierKnot(position, -dirIn, dirOut, GetKnotRotation(dirRot, normal));
}
internal static quaternion GetKnotRotation(float3 tangent, float3 normal)
{
if (math.lengthsq(tangent) == 0f)
tangent = math.rotate(Quaternion.FromToRotation(math.up(), normal), math.forward());
float3 up = Mathf.Approximately(math.abs(math.dot(math.normalizesafe(tangent), math.normalizesafe(normal))), 1f)
? math.cross(math.normalizesafe(tangent), math.right())
: Vector3.ProjectOnPlane(normal, tangent).normalized;
return quaternion.LookRotationSafe(math.normalizesafe(tangent), up);
}
///
/// Reset a transform position to a position while keeping knot positions in the same place. This modifies both
/// knot positions and transform position.
///
/// The target spline.
/// The point in world space to move the pivot to.
public static void SetPivot(SplineContainer container, Vector3 position)
{
var transform = container.transform;
var delta = position - transform.position;
transform.position = position;
var spline = container.Spline;
for (int i = 0, c = spline.Count; i < c; i++)
spline[i] = spline[i] - delta;
}
///
/// Computes a to approximate the curve formed by the list of provided points
/// within the given error threshold.
///
/// The list of points that define the curve to approximate.
/// The error threshold to use. Represents the largest distance between any of the
/// provided points and the curve.
/// Whether to close the .
/// The output fitted to the points, or an empty spline if could not be fitted.
/// Whether a curve could be fitted according to the provided error threshold.
public static bool FitSplineToPoints(List points, float errorThreshold, bool closed,
out Spline spline)
{
/*
Implementation based on:
"Algorithm for Automatically Fitting Digitized Curves"
by Philip J. Schneider
"Graphics Gems", Academic Press, 1990
*/
const float epsilon = 0.001f;
if (points == null || points.Count < 2)
{
Debug.LogError("FitSplineToPoints failed: The provided points list is either null, empty or contains only one point.");
spline = new Spline();
return false;
}
var pointsCopy = new List(points);
if (closed)
{
// As the base algorithm does not have a notion of closed splines, we add 2-3 extra/extension points to the input list
// to help the algorithm smooth out tangents at the connecting/closing point. They are removed from the resulting
// spline at the end of the processing.
// 1) If the first and last points do not match, we duplicate the first and add it to the end of the curve
if (math.length(pointsCopy[0] - pointsCopy[^1]) > epsilon)
pointsCopy.Add(pointsCopy[0]);
// 2) We then do a forward loop/revolution of one point by duplicating the second knot and attaching it to the end
pointsCopy.Add(pointsCopy[1]);
// 3) What was initially the last point, we duplicate and insert at front - backward loop/revolution of one point
pointsCopy.Insert(0, pointsCopy[^3]);
}
var leftTangent = math.normalize(pointsCopy[1] - pointsCopy[0]);
var rightTangent = math.normalize(pointsCopy[^2] - pointsCopy[^1]);
// Do recursive fitting
if (FitSplineToPointsStepInternal(pointsCopy, 0, pointsCopy.Count - 1, leftTangent, rightTangent, errorThreshold, closed, closed, out spline))
{
// For closed spline, clean up/merge knots that were added due to the appended extension points
if (closed && spline.Count > 2)
{
// Remove 2 of the 3 extension knots of closed splines.
spline.RemoveAt(0);
spline.RemoveAt(spline.Count - 1);
// Merge the overlaping front/end knots.
var firstKnot = spline[0];
firstKnot.TangentIn = spline[^1].TangentOut;
spline[0] = firstKnot;
spline.RemoveAt(spline.Count - 1);
spline.Closed = true;
}
// At this point we have the final shape of the spline - tangents can now be baked into rotation.
var up = float3.zero;
for (int i = 0; i < spline.Count; ++i)
{
var knot = spline[i];
var knotDir = knot.TangentOut;
if (math.lengthsq(knotDir) == 0f)
knotDir = spline.EvaluateTangent(GetNormalizedInterpolation(spline, i, PathIndexUnit.Knot));
var knotDirNorm = math.normalizesafe(knotDir);
if (i == 0)
up = CalculatePreferredNormalForDirection(knotDirNorm);
else
up = CurveUtility.EvaluateUpVector(spline.GetCurve(i - 1), 1f, up, float3.zero, false);
var tangentMode = TangentMode.Broken;
if (math.dot(math.normalizesafe(knot.TangentIn), math.normalizesafe(knot.TangentOut)) <= -1f + epsilon)
{
if (math.abs(math.length(knot.TangentIn) - math.length(knot.TangentOut)) <= epsilon)
tangentMode = TangentMode.Mirrored;
else
tangentMode = TangentMode.Continuous;
}
knot.TangentIn = new float3(0f, 0f, -math.length(knot.TangentIn));
knot.TangentOut = new float3(0f, 0f, math.length(knot.TangentOut));
knot.Rotation = quaternion.LookRotationSafe(knotDirNorm, up);
spline[i] = knot;
if (tangentMode != TangentMode.Broken)
spline.SetTangentModeNoNotify(i, tangentMode);
}
return true;
}
return false;
}
private static bool FitSplineToPointsStepInternal(IReadOnlyList allPoints, int rangeStart, int rangeEnd, float3 leftTangent, float3 rightTangent, float errorThreshold, bool closed, bool splineClosed,
out Spline spline)
{
const int maxIterations = 4;
spline = new Spline();
var curRangePointCount = rangeEnd - rangeStart + 1;
if (curRangePointCount < 2)
return false;
if (curRangePointCount == 2)
{
var difference = allPoints[rangeStart] - allPoints[rangeEnd];
//we use 1/3 of the distance between each of the points as described in the graphics gems paper.
var diffLength = math.length(difference) / 3f;
var leftTanScaled = leftTangent * diffLength;
var rightTanScaled = rightTangent * diffLength;
var firstKnot = new BezierKnot(allPoints[rangeStart], -leftTanScaled, leftTanScaled, Quaternion.identity);
var secondKnot = new BezierKnot(allPoints[rangeStart + 1], rightTanScaled, -rightTanScaled, Quaternion.identity);
spline = new Spline();
spline.Add(firstKnot, TangentMode.Broken);
spline.Add(secondKnot, TangentMode.Broken);
spline.Closed = closed;
return true;
}
// Find corresponding t values for each point so we can compute errors (chord-length parameterization)
var correspondingTValues = new float[curRangePointCount];
var firstPassTValues = new float[curRangePointCount];
firstPassTValues[0] = 0f;
var cumulativeChordLengths = new float[curRangePointCount - 1];
var chordLengthSum = 0f;
for (int i = 1; i < curRangePointCount; i++)
{
var chord = allPoints[rangeStart + i] - allPoints[rangeStart + i - 1];
chordLengthSum += math.length(chord);
cumulativeChordLengths[i - 1] = chordLengthSum;
}
for (int i = 0; i < cumulativeChordLengths.Length; i++)
{
firstPassTValues[i + 1] = cumulativeChordLengths[i] / chordLengthSum;
}
correspondingTValues = firstPassTValues;
spline = GenerateSplineFromTValues(allPoints, rangeStart, rangeEnd, closed, correspondingTValues, leftTangent, rightTangent);
var tPositions = new float3[curRangePointCount];
for (int i = 0; i < correspondingTValues.Length; i++)
{
float t = correspondingTValues[i];
float3 position = spline.EvaluatePosition(t);
tPositions[i] = position;
}
var errorResult = ComputeMaxError(allPoints, rangeStart, rangeEnd, tPositions, errorThreshold, splineClosed);
var errorBeforeReparameterization = errorResult.maxError;
var splitPoint = errorResult.maxErrorIndex;
if (errorBeforeReparameterization < errorThreshold)
return true;
// If error is small enough, try reparameterizing and fitting again
else if (errorBeforeReparameterization < 4 * errorThreshold)
{
var numIterations = 0;
while (numIterations < maxIterations)
{
var curveKnots = new BezierKnot[spline.Count];
int i = 0;
foreach (BezierKnot bezierKnot in spline.Knots)
{
curveKnots[i] = bezierKnot;
++i;
}
var P0 = curveKnots[0].Position;
var P1 = curveKnots[0].Position + curveKnots[0].TangentOut;
var P2 = curveKnots[1].Position + curveKnots[1].TangentIn;
var P3 = curveKnots[1].Position;
var P = new float3[] { P0, P1, P2, P3 };
var q1 = new float3[3];
for (int j = 0; j <= 2; ++j)
{
q1[j] = (P[j + 1] - P[j]) * 3f;
}
var q2 = new float3[2];
for (int j = 0; j <= 1; ++j)
{
q2[j] = (q1[j + 1] - q1[j]) * 2f;
}
for (int k = rangeStart; k < curRangePointCount-1; k++)
{
var t = correspondingTValues[k];
var q = Bernstein(t, P, 3);
var q1_t = Bernstein(t, q1, 2);
var q2_t = Bernstein(t, q2, 1);
var numerator = math.dot(q - tPositions[k], q1_t);
var denominator = math.dot(q1_t, q1_t) + math.dot(q - tPositions[k], q2_t);
if (denominator != 0)
correspondingTValues[k] -= (numerator / denominator);
}
spline = GenerateSplineFromTValues(allPoints, rangeStart, rangeEnd, closed, correspondingTValues, leftTangent, rightTangent);
for (int k = 0; k < tPositions.Length; k++)
{
var t = correspondingTValues[k];
var position = spline.EvaluatePosition(t);
tPositions[k] = position;
}
errorResult = ComputeMaxError(allPoints, rangeStart, rangeEnd, tPositions, errorThreshold, splineClosed);
var errorAfterReparameterization = errorResult.maxError;
splitPoint = errorResult.maxErrorIndex;
if (errorAfterReparameterization < errorThreshold)
{
return true;
}
numIterations++;
}
}
// Still not good enough. Try splitting at point of max error.
if (curRangePointCount == 3)
splitPoint = rangeStart + 1;
else
{
if (splitPoint == 0)
splitPoint++;
else if (splitPoint == curRangePointCount - 1)
splitPoint--;
}
var splitTangent = CalculateCenterTangent(allPoints[splitPoint - 1], allPoints[splitPoint], allPoints[splitPoint + 1]);
if (!FitSplineToPointsStepInternal(allPoints, rangeStart, splitPoint, leftTangent, splitTangent, errorThreshold, false, splineClosed, out var firstSpline) ||
!FitSplineToPointsStepInternal(allPoints, splitPoint, rangeEnd, -splitTangent, rightTangent, errorThreshold, false, splineClosed, out var secondSpline))
{
spline = new Spline();
return false;
}
var splitPointIdx = firstSpline.Count - 1;
var splitPointTangentIn = firstSpline[^1].TangentIn;
firstSpline.RemoveAt(splitPointIdx);
firstSpline.Add(secondSpline);
spline = firstSpline;
var splitKnot = spline[splitPointIdx];
splitKnot.TangentIn = splitPointTangentIn;
spline[splitPointIdx] = splitKnot;
return true;
}
static float3 CalculatePreferredNormalForDirection(float3 splineDirection)
{
var dirNorm = math.normalizesafe(splineDirection);
float3 absDotRUF;
absDotRUF.x = math.abs(math.dot(dirNorm, math.right()));
absDotRUF.y = math.abs(math.dot(dirNorm, math.up()));
absDotRUF.z = math.abs(math.dot(dirNorm, math.forward()));
float3 right;
// Pick the right vector based on a world axis that the spline tangent is most orthogonal to
if (absDotRUF.x < absDotRUF.y && absDotRUF.x < absDotRUF.z)
right = math.normalizesafe(math.cross(math.right(), dirNorm));
else if (absDotRUF.y < absDotRUF.x && absDotRUF.y < absDotRUF.z)
right = math.normalizesafe(math.cross(math.up(), dirNorm));
else
right = math.normalizesafe(math.cross(math.forward(), dirNorm));
return math.normalizesafe(math.cross(dirNorm, right));
}
static float3 CalculateCenterTangent(float3 prevPoint, float3 centerPoint, float3 nextPoint)
{
var v1 = prevPoint - centerPoint;
var v2 = centerPoint - nextPoint;
var centerTangent = math.normalize((v1 + v2) / 2.0f);
return centerTangent;
}
//A helper method for FitSplineToPoints that computes bernstein basis functions
static float3 Bernstein(float t, float3[] bezier, int degree)
{
var copy = new float3[bezier.Length];
bezier.CopyTo(copy, 0);
for (int i = 1; i <= degree; ++i)
{
for (int j = 0; j <= degree - i; ++j)
{
copy[j] = (1 - t) * copy[j] + t * copy[j + 1];
}
}
return copy[0];
}
//A helper method for FitSplineToPoints that generates a spline from a provided array of
// interpolation values.
static Spline GenerateSplineFromTValues(IReadOnlyList allPoints, int rangeStart, int rangeEnd, bool closed, float[] tValues, float3 leftTangent, float3 rightTangent)
{
var curRangePointCount = rangeEnd - rangeStart + 1;
var aLeft = new float3[curRangePointCount];
var aRight = new float3[curRangePointCount];
for (int i = 0; i < curRangePointCount; ++i)
{
var t = tValues[i];
aLeft[i] = leftTangent * 3f * t * (1f - t) * (1f - t);
aRight[i] = rightTangent * 3f * t * t * (1f - t);
}
var c = new float[2, 2];
float x1 = 0f, x2 = 0f;
for (int i = 0; i < curRangePointCount; ++i)
{
var t = tValues[i];
c[0, 0] += math.dot(aLeft[i], aLeft[i]);
c[0, 1] += math.dot(aLeft[i], aRight[i]);
c[1, 0] = c[0, 1];
c[1, 1] += math.dot(aRight[i], aRight[i]);
var tmp = allPoints[rangeStart + i] - (
allPoints[rangeStart] * (1f - t) * (1f - t) * (1f - t)
+ allPoints[rangeStart] * 3f * t * (1f - t) * (1f - t)
+ allPoints[rangeEnd] * 3f * t * t * (1f - t)
+ allPoints[rangeEnd] * t * t * t
);
x1 += math.dot(aLeft[i], tmp);
x2 += math.dot(aRight[i], tmp);
}
var c0_c1_determinant = c[0, 0] * c[1, 1] - c[1, 0] * c[0, 1];
var c0_x_determinant = c[0, 0] * x2 - c[1, 0] * x1;
var x_c1_determinant = x1 * c[1, 1] - x2 * c[0, 1];
//now we compute the coefficients for our tangents
var alpha1 = c0_c1_determinant == 0 ? 0 : x_c1_determinant / c0_c1_determinant;
var alpha2 = c0_c1_determinant == 0 ? 0 : c0_x_determinant / c0_c1_determinant;
// alpha1 and alpha2 can be zero and in that case the original algorithm just falls back to the value
// of 1/3 distance between the range's first and last points. Omitting this can introduce
// zero tangent curves that break the resulting spline's continuity around certain knots.
var segLength = math.length(allPoints[rangeEnd] - allPoints[rangeStart]);
var epsilon = 1.0e-6f * segLength;
if (alpha1 < epsilon || alpha2 < epsilon)
alpha1 = alpha2 = segLength / 3f;
var first = new BezierKnot(allPoints[rangeStart], leftTangent * -alpha1, leftTangent * alpha1, quaternion.identity);
var second = new BezierKnot(allPoints[rangeEnd], rightTangent * alpha2, rightTangent * -alpha2, quaternion.identity);
var spline = new Spline();
// Use broken tangent modes for now as we'll pick the correct mode once the spline curvature is finalized.
spline.Add(first, TangentMode.Broken);
spline.Add(second, TangentMode.Broken);
spline.Closed = closed;
return spline;
}
// Helper function for FitSplineToPoints that computes the maximum least squares distance error
// and the index of the point for which the error is maximum.
static (float maxError, int maxErrorIndex) ComputeMaxError(IReadOnlyList allPoints, int rangeStart, int rangeEnd, float3[] positions, float errorThreshold, bool splineClosed)
{
var rangeCount = rangeEnd - rangeStart + 1;
// For closed spline, we force-split the fit curve at the extra/extension points that we add for closed spline points at the beginning.
// This gives the first and last knots additional adjacent knots that help the algorithm to smoothly unify the spline
// at the closing point (essentially treating closed splines as open splines that "loop" one extra knot forward and two extra knots backward).
// This also ensures that for all of the extension points that were added, there will be matching temporary spline knots
// that we can safely remove/merge at the end of the process.
if (splineClosed && rangeCount > 2)
{
var secondKnotIdx = 1;
if (rangeStart < secondKnotIdx && rangeEnd > secondKnotIdx)
return (errorThreshold, secondKnotIdx);
var knotBeforeLastIdx = allPoints.Count - 2;
if (rangeStart < knotBeforeLastIdx && rangeEnd > knotBeforeLastIdx)
return (errorThreshold, knotBeforeLastIdx);
}
// Use least squares difference to evaluate error.
var maxError = 0f;
var splitPoint = 0;
for (int i = 0; i < rangeCount; ++i)
{
var globalIndex = rangeStart + i;
var difference = allPoints[globalIndex] - positions[i];
var error = math.length(difference);
if (error > maxError)
{
maxError = error;
splitPoint = globalIndex;
}
}
return (maxError, splitPoint);
}
///
/// Creates a new spline and adds it to the .
///
/// The target container.
/// A type that implements .
/// Returns the spline that was created and added to the container.
public static Spline AddSpline(this T container) where T : ISplineContainer
{
var spline = new Spline();
AddSpline(container, spline);
return spline;
}
///
/// Add a new to the .
///
/// The target container.
/// The spline to append to this container.
/// A type that implements .
public static void AddSpline(this T container, Spline spline) where T : ISplineContainer
{
var splines = new List(container.Splines);
splines.Add(spline);
container.Splines = splines;
}
///
/// Removes a spline from a .
///
/// The target container.
/// The index of the spline to remove from the SplineContainer.
/// A type that implements .
/// Returns true if the spline was removed from the container.
public static bool RemoveSplineAt(this T container, int splineIndex) where T : ISplineContainer
{
if (splineIndex < 0 || splineIndex >= container.Splines.Count)
return false;
var splines = new List(container.Splines);
splines.RemoveAt(splineIndex);
container.KnotLinkCollection.SplineRemoved(splineIndex);
container.Splines = splines;
return true;
}
///
/// Removes a spline from a .
///
/// The target SplineContainer.
/// The spline to remove from the SplineContainer.
/// A type that implements .
/// Returns true if the spline was removed from the container.
public static bool RemoveSpline(this T container, Spline spline) where T : ISplineContainer
{
var splines = new List(container.Splines);
var index = splines.IndexOf(spline);
if (index < 0)
return false;
splines.RemoveAt(index);
container.KnotLinkCollection.SplineRemoved(index);
container.Splines = splines;
return true;
}
///
/// Reorders a spline in a .
///
/// The target SplineContainer.
/// The previous index of the spline to reorder in the SplineContainer.
/// The new index of the spline to reorder in the SplineContainer.
/// A type that implements .
/// Returns true if the spline was reordered in the container.
public static bool ReorderSpline(this T container, int previousSplineIndex, int newSplineIndex) where T : ISplineContainer
{
if (previousSplineIndex < 0 || previousSplineIndex >= container.Splines.Count ||
newSplineIndex < 0 || newSplineIndex >= container.Splines.Count)
return false;
var splines = new List(container.Splines);
var spline = splines[previousSplineIndex];
splines.RemoveAt(previousSplineIndex);
splines.Insert(newSplineIndex, spline);
container.KnotLinkCollection.SplineIndexChanged(previousSplineIndex, newSplineIndex);
container.Splines = splines;
return true;
}
internal static bool IsIndexValid(T container, SplineKnotIndex index) where T : ISplineContainer
{
return index.Knot >= 0 && index.Knot < container.Splines[index.Spline].Count &&
index.Spline < container.Splines.Count && index.Knot < container.Splines[index.Spline].Count;
}
///
/// Sets the position of all knots linked to the knot at `index` in an to the same position.
///
/// The target container.
/// The `SplineKnotIndex` of the knot to use to synchronize the positions.
/// A type that implements .
public static void SetLinkedKnotPosition(this T container, SplineKnotIndex index) where T : ISplineContainer
{
if (!container.KnotLinkCollection.TryGetKnotLinks(index, out var knots))
return;
var splines = container.Splines;
var position = splines[index.Spline][index.Knot].Position;
foreach (var i in knots)
{
if (!IsIndexValid(container, i))
return;
var knot = splines[i.Spline][i.Knot];
var originalPosition = knot.Position;
knot.Position = position;
splines[i.Spline].SetKnotNoNotify(i.Knot, knot);
// If the knot has been moved, notifies a knotModified event
if (!Mathf.Approximately(Vector3.Distance(position, originalPosition), 0f))
splines[i.Spline].SetDirty(SplineModification.KnotModified, i.Knot);
}
}
///
/// Links two knots in an . The two knots can be on different splines, but both must be in the referenced SplineContainer.
/// If these knots are linked to other knots, all existing links are kept and updated.
///
/// The target SplineContainer.
/// The first knot to link.
/// A type that implements .
/// The second knot to link.
public static void LinkKnots(this T container, SplineKnotIndex knotA, SplineKnotIndex knotB) where T : ISplineContainer
{
bool similarPositions = Mathf.Approximately(math.length(container.Splines[knotA.Spline][knotA.Knot].Position - container.Splines[knotB.Spline][knotB.Knot].Position), 0f);
var knotsToNotify = similarPositions ? null : container.KnotLinkCollection.GetKnotLinks(knotB);
container.KnotLinkCollection.Link(knotA, knotB);
if (knotsToNotify != null)
{
foreach (var ski in knotsToNotify)
container.Splines[ski.Spline].SetDirty(SplineModification.KnotModified, ski.Knot);
}
}
///
/// Unlinks several knots from an . A knot in `knots` disconnects from other knots it was linked to.
///
/// The target SplineContainer.
/// The knot to unlink.
/// A type implementing .
public static void UnlinkKnots(this T container, IReadOnlyList knots) where T : ISplineContainer
{
foreach (var knot in knots)
container.KnotLinkCollection.Unlink(knot);
}
///
/// Checks if two knots from an are linked together.
///
/// The target SplineContainer.
/// The first knot to check.
/// The second knot to check against.
/// Returns true if and are linked; otherwise, false.
public static bool AreKnotLinked(this ISplineContainer container, SplineKnotIndex knotA, SplineKnotIndex knotB)
{
if (!container.KnotLinkCollection.TryGetKnotLinks(knotA, out var linkedKnots))
return false;
for (int i = 0; i < linkedKnots.Count; ++i)
if (linkedKnots[i] == knotB)
return true;
return false;
}
///
/// Copies knot links between two splines of the same .
///
/// The target SplineContainer.
/// The index of the source spline to copy from.
/// The index of the destination spline to copy to.
/// A type implementing .
///
/// The knot links will only be copied if both of the spline indices are valid and both splines have the same amount of knots.
///
public static void CopyKnotLinks(this T container, int srcSplineIndex, int destSplineIndex) where T : ISplineContainer
{
if ((srcSplineIndex < 0 || srcSplineIndex >= container.Splines.Count) ||
(destSplineIndex < 0 || destSplineIndex >= container.Splines.Count))
return;
var srcSpline = container.Splines[srcSplineIndex];
var dstSpline = container.Splines[destSplineIndex];
if (srcSpline.Count == 0 || srcSpline.Count != dstSpline.Count)
return;
for (int i = 0, c = srcSpline.Count; i < c; ++i)
{
if (container.KnotLinkCollection.TryGetKnotLinks(new SplineKnotIndex(srcSplineIndex, i), out _))
container.KnotLinkCollection.Link(new SplineKnotIndex(srcSplineIndex, i), new SplineKnotIndex(destSplineIndex, i));
}
}
///
/// Removes redundant points in a poly line to form a similar shape with fewer points.
///
/// The poly line to act on.
/// The maximum distance from the reduced poly line shape for a point to be discarded.
/// The collection type. Usually this will be list or array of float3.
/// Returns a new list with a poly line matching the shape of the original line with fewer points.
public static List ReducePoints(T line, float epsilon = .15f) where T : IList
{
var ret = new List();
var rdp = new RamerDouglasPeucker(line);
rdp.Reduce(ret, epsilon);
return ret;
}
///
/// Removes redundant points in a poly line to form a similar shape with fewer points.
///
/// The poly line to act on.
/// A pre-allocated list to be filled with the new reduced line points.
/// The maximum distance from the reduced poly line shape for a point to be discarded.
/// The collection type. Usually this will be list or array of float3.
public static void ReducePoints(T line, List results, float epsilon = .15f) where T : IList
{
var rdp = new RamerDouglasPeucker(line);
rdp.Reduce(results, epsilon);
}
internal static bool AreTangentsModifiable(TangentMode mode)
{
return mode == TangentMode.Broken || mode == TangentMode.Continuous || mode == TangentMode.Mirrored;
}
///
/// Reverses the flow direction of a spline.
///
/// The target SplineContainer.
/// The index of the spline to reverse.
public static void ReverseFlow(this ISplineContainer container, int splineIndex)
{
ReverseFlow(new SplineInfo(container, splineIndex));
}
///
/// Reverses the flow direction of a spline.
///
/// The spline to reverse.
public static void ReverseFlow(SplineInfo splineInfo)
{
var spline = splineInfo.Spline;
var knots = splineInfo.Spline.ToArray();
var tangentModes = new TangentMode[spline.Count];
for (int i = 0; i < tangentModes.Length; ++i)
tangentModes[i] = spline.GetTangentMode(i);
var splineLinks = new List>();
// GetAll LinkedKnots on the spline
for(int previousKnotIndex = 0; previousKnotIndex < spline.Count; ++previousKnotIndex)
{
var knot = new SplineKnotIndex(splineInfo.Index, previousKnotIndex);
var collection = splineInfo.Container.KnotLinkCollection.GetKnotLinks(knot).ToList();
splineLinks.Add(collection);
}
//Unlink all knots in the spline
foreach(var linkedKnots in splineLinks)
splineInfo.Container.UnlinkKnots(linkedKnots);
// Reverse order and tangents
for (int previousKnotIndex = 0; previousKnotIndex < spline.Count; ++previousKnotIndex)
{
var knot = knots[previousKnotIndex];
var worldKnot = knot.Transform(splineInfo.LocalToWorld);
var tangentIn = worldKnot.TangentIn;
var tangentOut = worldKnot.TangentOut;
var reverseRotation = quaternion.AxisAngle(math.mul(knot.Rotation, math.up()), math.radians(180));
reverseRotation = math.normalizesafe(reverseRotation);
// Reverse the tangents to keep the same shape while reversing the order
knot.Rotation = math.mul(reverseRotation, knot.Rotation);
if(tangentModes[previousKnotIndex] is TangentMode.Broken)
{
var localRot = quaternion.AxisAngle(math.up(), math.radians(180));
knot.TangentIn = math.rotate(localRot, tangentOut);
knot.TangentOut = math.rotate(localRot, tangentIn);
}
else if(tangentModes[previousKnotIndex] is TangentMode.Continuous)
{
knot.TangentIn = -tangentOut;
knot.TangentOut = -tangentIn;
}
var newKnotIndex = spline.Count - 1 - previousKnotIndex;
spline.SetTangentMode(newKnotIndex, tangentModes[previousKnotIndex]);
spline[newKnotIndex] = knot;
}
//Redo all links
foreach (var linkedKnots in splineLinks)
{
if(linkedKnots.Count == 1)
continue;
var originalKnot = linkedKnots[0];
originalKnot = new SplineKnotIndex(originalKnot.Spline,
originalKnot.Spline.Equals(splineInfo.Index) ? spline.Count - 1 - originalKnot.Knot : originalKnot.Knot);
for(int i = 1; i < linkedKnots.Count; ++i)
{
var knotInfo = linkedKnots[i];
if (knotInfo.Spline.Equals(splineInfo.Index))
linkedKnots[i] = new SplineKnotIndex(splineInfo.Index, spline.Count - 1 - knotInfo.Knot);
splineInfo.Container.LinkKnots(originalKnot, linkedKnots[i]);
}
}
}
///
/// Reverses the flow direction of a spline. Should only be used with Splines that aren't inside any container.
///
/// The spline to reverse.
public static void ReverseFlow(Spline spline)
{
var knots = spline.ToArray();
var tangentModes = new TangentMode[spline.Count];
for (int i = 0; i < tangentModes.Length; ++i)
tangentModes[i] = spline.GetTangentMode(i);
// Reverse order and tangents
for (int previousKnotIndex = 0; previousKnotIndex < spline.Count; ++previousKnotIndex)
{
var knot = knots[previousKnotIndex];
var tangentIn = knot.TangentIn;
var tangentOut = knot.TangentOut;
var reverseRotation = quaternion.AxisAngle(math.mul(knot.Rotation, math.up()), math.radians(180));
reverseRotation = math.normalizesafe(reverseRotation);
// Reverse the tangents to keep the same shape while reversing the order
knot.Rotation = math.mul(reverseRotation, knot.Rotation);
if (tangentModes[previousKnotIndex] is TangentMode.Broken)
{
var localRot = quaternion.AxisAngle(math.up(), math.radians(180));
knot.TangentIn = math.rotate(localRot, tangentOut);
knot.TangentOut = math.rotate(localRot, tangentIn);
}
else if (tangentModes[previousKnotIndex] is TangentMode.Continuous)
{
knot.TangentIn = -tangentOut;
knot.TangentOut = -tangentIn;
}
var newKnotIndex = spline.Count - 1 - previousKnotIndex;
spline.SetTangentMode(newKnotIndex, tangentModes[previousKnotIndex]);
spline[newKnotIndex] = knot;
}
}
///
/// Joins two splines together at the specified knots. The two splines must belong to the same container and
/// the knots must be at an extremity of their respective splines.
///
/// The target SplineContainer.
/// The first spline extremity to join.
/// The second spline extremity to join.
/// The `SplineKnotIndex` of the junction knot.
///
/// In case a spline needs to be reversed to join the two extremities, the mainKnot defines which spline will be kept.
/// Hence, the second one will be the reversed one.
///
///
/// An exception is thrown on impossible join request (out of bounds
/// parameters, knots on the same spline, non-extremity knots)
///
public static SplineKnotIndex JoinSplinesOnKnots(this ISplineContainer container, SplineKnotIndex mainKnot, SplineKnotIndex otherKnot)
{
if (mainKnot.Spline == otherKnot.Spline)
{
Debug.LogError("Trying to join Knots already belonging to the same spline.", container as SplineContainer);
return new SplineKnotIndex();
}
if (mainKnot.Spline < 0 || mainKnot.Spline > container.Splines.Count)
{
Debug.LogError($"Spline index {mainKnot.Spline} does not exist for the current container.", container as SplineContainer);
return new SplineKnotIndex();
}
if(otherKnot.Spline < 0 || otherKnot.Spline > container.Splines.Count)
{
Debug.LogError($"Spline index {otherKnot.Spline} does not exist for the current container.", container as SplineContainer);
return new SplineKnotIndex();
}
if(mainKnot.Knot < 0 || mainKnot.Knot > container.Splines[mainKnot.Spline].Count)
{
Debug.LogError($"Knot index {mainKnot.Knot} does not exist for the current container for Spline[{mainKnot.Spline}].", container as SplineContainer);
return new SplineKnotIndex();
}
if(otherKnot.Knot < 0 || otherKnot.Knot > container.Splines[otherKnot.Spline].Count)
{
Debug.LogError($"Knot index {otherKnot.Knot} does not exist for the current container for Spline[{otherKnot.Spline}].", container as SplineContainer);
return new SplineKnotIndex();
}
if(mainKnot.Knot != 0 && mainKnot.Knot != container.Splines[mainKnot.Spline].Count - 1)
{
Debug.LogError($"Knot index {mainKnot.Knot} is not an extremity knot for the current container for Spline[{mainKnot.Spline}]." +
"Only extremity knots can be joined.", container as SplineContainer);
return new SplineKnotIndex();
}
if(otherKnot.Knot != 0 && otherKnot.Knot != container.Splines[otherKnot.Spline].Count - 1)
{
Debug.LogError($"Knot index {otherKnot.Knot} is not an extremity knot for the current container for Spline[{otherKnot.Spline}]." +
"Only extremity knots can be joined.", container as SplineContainer);
return new SplineKnotIndex();
}
var isActiveKnotAtStart = mainKnot.Knot == 0;
var isOtherKnotAtStart = otherKnot.Knot == 0;
//Reverse spline if needed, this is needed when the 2 knots are both starts or ends of their respective spline
if(isActiveKnotAtStart == isOtherKnotAtStart)
//We give more importance to the main knot, so we reverse the spline associated to otherKnot
container.ReverseFlow(otherKnot.Spline);
//Save Links
var links = new List>();
//Save relevant data before joining the splines
var activeSplineIndex = mainKnot.Spline;
var activeSpline = container.Splines[activeSplineIndex];
var activeSplineCount = activeSpline.Count;
var otherSplineIndex = otherKnot.Spline;
var otherSpline = container.Splines[otherSplineIndex];
var otherSplineCount = otherSpline.Count;
// Get all LinkedKnots on the splines
for(int i = 0; i < activeSplineCount; ++i)
{
var knot = new SplineKnotIndex(mainKnot.Spline, i);
links.Add(container.KnotLinkCollection.GetKnotLinks(knot).ToList());
}
for(int i = 0; i < otherSplineCount; ++i)
{
var knot = new SplineKnotIndex(otherKnot.Spline, i);
links.Add(container.KnotLinkCollection.GetKnotLinks(knot).ToList());
}
//Unlink all knots in the spline
foreach(var linkedKnots in links)
container.UnlinkKnots(linkedKnots);
if(otherSplineCount > 1)
{
//Join Splines
if(isActiveKnotAtStart)
{
//All position from the other spline must be added before the knot A
//Don't copy the last knot of the other spline as this is the one to join
for(int i = otherSplineCount - 2; i >= 0 ; i--)
activeSpline.Insert(0,otherSpline[i], otherSpline.GetTangentMode(i));
}
else
{
//All position from the other spline must be added after the knot A
//Don't copy the first knot of the other spline as this is the one to join
for(int i = 1; i < otherSplineCount; i++)
activeSpline.Add(otherSpline[i], otherSpline.GetTangentMode(i));
}
}
container.RemoveSplineAt(otherSplineIndex);
var newActiveSplineIndex = otherSplineIndex > activeSplineIndex ? activeSplineIndex : mainKnot.Spline - 1;
//Restore links
foreach (var linkedKnots in links)
{
if(linkedKnots.Count == 1)
continue;
for(int i = 0; i < linkedKnots.Count; ++i)
{
var knotInfo = linkedKnots[i];
if(knotInfo.Spline == activeSplineIndex || knotInfo.Spline == otherSplineIndex)
{
var newIndex = knotInfo.Knot;
if(knotInfo.Spline == activeSplineIndex && isActiveKnotAtStart)
newIndex += otherSplineCount - 1;
if(knotInfo.Spline == otherSplineIndex && !isActiveKnotAtStart)
newIndex += activeSplineCount - 1;
linkedKnots[i] = new SplineKnotIndex(activeSplineIndex, newIndex);
}
else
{
if(knotInfo.Spline > otherSplineIndex)
linkedKnots[i] = new SplineKnotIndex(knotInfo.Spline - 1,knotInfo.Knot);
}
}
var originalKnot = linkedKnots[0];
for(int i = 1; i < linkedKnots.Count; ++i)
container.LinkKnots(originalKnot, linkedKnots[i]);
}
return new SplineKnotIndex(newActiveSplineIndex, isActiveKnotAtStart ? otherSplineCount - 1 : mainKnot.Knot);
}
internal static SplineKnotIndex DuplicateKnot(this ISplineContainer container, SplineKnotIndex originalKnot, int targetIndex)
{
var spline = container.Splines[originalKnot.Spline];
var knot = spline[originalKnot.Knot];
spline.Insert(targetIndex, knot);
spline.SetTangentMode(targetIndex, spline.GetTangentMode(originalKnot.Knot));
return new SplineKnotIndex(originalKnot.Spline, targetIndex);
}
///
/// Duplicate a spline between 2 knots of a source spline.
///
/// The target SplineContainer.
/// The start knot to use to duplicate the spline.
/// The end knot to use to duplicate the spline.
/// The index of the new created spline in the container.
/// Thrown when the provided knots aren't valid or aren't on the same spline.
public static void DuplicateSpline(
this ISplineContainer container,
SplineKnotIndex fromKnot,
SplineKnotIndex toKnot,
out int newSplineIndex)
{
newSplineIndex = -1;
if (!(fromKnot.IsValid() && toKnot.IsValid()))
throw new ArgumentException("Duplicate failed: The 2 provided knots must be valid knots.");
if(fromKnot.Spline != toKnot.Spline)
throw new ArgumentException("Duplicate failed: The 2 provided knots must be on the same Spline.");
var duplicate = container.AddSpline();
//Copy knots to the new spline
int startIndex = Math.Min(fromKnot.Knot, toKnot.Knot);
int toIndex = Math.Max(fromKnot.Knot, toKnot.Knot);
var originalSplineIndex = fromKnot.Spline;
var originalSpline = container.Splines[originalSplineIndex];
newSplineIndex = container.Splines.Count - 1;
for (int i = startIndex; i <= toIndex; ++i)
{
duplicate.Add(originalSpline[i], originalSpline.GetTangentMode(i));
// If the old knot had any links we link both old and new knot.
// This will result in the new knot linking to what the old knot was linked to.
// The old knot being removed right after that takes care of cleaning the old knot from the link.
if (container.KnotLinkCollection.TryGetKnotLinks(new SplineKnotIndex(originalSplineIndex, i), out _))
container.KnotLinkCollection.Link(new SplineKnotIndex(originalSplineIndex, i), new SplineKnotIndex(newSplineIndex, i - startIndex));
}
}
///
/// Splits a spline in a SplineContainer into two splines at a specified knot.
///
/// The target SplineContainer.
/// The SplineKnotIndex of the spline to split.
/// The `SplineKnotIndex` of the first knot of the spline that was created from the split.
///
/// An exception is thrown when the knot belongs to a spline not contained by
/// the provided container or when the knot index is out of range.
///
public static SplineKnotIndex SplitSplineOnKnot(this ISplineContainer container, SplineKnotIndex knotInfo)
{
if (knotInfo.Spline < 0 || knotInfo.Spline > container.Splines.Count)
{
throw new IndexOutOfRangeException($"Spline index {knotInfo.Spline} does not exist for the current container.");
}
if (knotInfo.Knot < 0 || knotInfo.Knot > container.Splines[knotInfo.Spline].Count)
{
throw new IndexOutOfRangeException($"Knot index {knotInfo.Knot} does not exist for the current container for Spline[{knotInfo.Spline}].");
}
var originalSpline = container.Splines[knotInfo.Spline];
if (originalSpline.Closed)
{
// Unclose and add a knot to the end with the same data
originalSpline.Closed = false;
var firstKnot = new SplineKnotIndex(knotInfo.Spline, 0);
var lastKnot = container.DuplicateKnot(firstKnot, originalSpline.Count);
// If the knot was the first one of the spline nothing else needs to be done to split the knot
if (knotInfo.Knot == 0)
return firstKnot;
// If the knot wasn't the first one we also need need to link both ends of the spline to keep the same spline we had before
// Link knots is recording the changes to prefab instances so we don't need to add a call to RecordPrefabInstancePropertyModifications
container.LinkKnots(firstKnot, lastKnot);
}
// If not closed, split does nothing on the extremity of the spline
else if (knotInfo.Knot == 0 || knotInfo.Knot == originalSpline.Count - 1)
return knotInfo;
container.DuplicateSpline(knotInfo, new SplineKnotIndex(knotInfo.Spline, originalSpline.Count - 1),
out int newSplineIndex);
originalSpline.Resize(knotInfo.Knot + 1);
return new SplineKnotIndex(newSplineIndex, 0);
}
}
}