using Godot;
using Rokojori;
using System.Collections.Generic;
using Rokojori.Extensions;
namespace Rokojori
{
/** Arc-length reparametrization of a SplineCurve.
A spline's own parameter is the point index, and a rational cubic bezier
does not travel at a constant rate along it - so stepping the point index
evenly gives rows that bunch up in the tight parts of the curve and spread
out in the straight ones. Anything that lays a texture, a mesh or a
placement along a spline wants to step by distance
instead, and there is no closed form for that: the only way is to sample
the curve, accumulate chord lengths and invert the table.
This class is that table. Create it once per curve state, then convert
freely in both directions.
`samplesPerSegment` is the accuracy knob. The table's length is always a
slight underestimate, because chords cut the corner off
the curve they approximate - 16 samples per segment is well under a
percent for anything that is not a hairpin.
*/
public class SplineArcLengthTable
{
SplineCurve _curve;
int _samplesPerSegment;
/** Cumulative distance at sample i, which sits at point index
i / samplesPerSegment. Monotonically non-decreasing by construction,
which is what makes the binary search in PointIndexAtDistance valid.
*/
float[] _distances;
public SplineCurve curve => _curve;
public float length => _distances.Length == 0 ? 0 : _distances[ _distances.Length - 1 ];
public int numSegments => Mathf.Max( 0, _curve.points.Count - 1 );
public static SplineArcLengthTable Create( SplineCurve curve, int samplesPerSegment = 16 )
{
var table = new SplineArcLengthTable();
table._curve = curve;
table._samplesPerSegment = Mathf.Max( 1, samplesPerSegment );
table.Bake();
return table;
}
void Bake()
{
var segments = numSegments;
if ( segments <= 0 )
{
_distances = new float[]{ 0 };
return;
}
var numSamples = segments * _samplesPerSegment + 1;
_distances = new float[ numSamples ];
var last = _curve.GetPositionByPointIndex( 0 );
var distance = 0f;
_distances[ 0 ] = 0;
for ( var i = 1; i < numSamples; i++ )
{
var pointIndex = i / (float) _samplesPerSegment;
var position = _curve.GetPositionByPointIndex( pointIndex );
distance += ( position - last ).Length();
_distances[ i ] = distance;
last = position;
}
}
/** Distance along the curve at a point index, interpolated between the two
surrounding samples.
*/
public float DistanceAtPointIndex( float pointIndex )
{
var maxIndex = _distances.Length - 1;
var sample = Mathf.Clamp( pointIndex * _samplesPerSegment, 0, maxIndex );
var lower = Mathf.FloorToInt( sample );
var upper = Mathf.Min( lower + 1, maxIndex );
return Mathf.Lerp( _distances[ lower ], _distances[ upper ], sample - lower );
}
/** The point index a distance along the curve lands on - the inverse of
DistanceAtPointIndex, by binary search plus a linear step inside the
sample it lands in.
Distances outside the curve clamp to its ends rather than extrapolating.
*/
public float PointIndexAtDistance( float distance )
{
var maxIndex = _distances.Length - 1;
if ( maxIndex <= 0 || distance <= 0 )
{
return 0;
}
if ( distance >= length )
{
return maxIndex / (float) _samplesPerSegment;
}
var low = 0;
var high = maxIndex;
// Invariant: _distances[ low ] <= distance < _distances[ high ].
while ( high - low > 1 )
{
var middle = ( low + high ) / 2;
if ( _distances[ middle ] <= distance )
{
low = middle;
}
else
{
high = middle;
}
}
var span = _distances[ high ] - _distances[ low ];
// Two coincident samples - a zero-length span has no inside to land in.
var fraction = span <= 0.000001f ? 0 : ( distance - _distances[ low ] ) / span;
return ( low + fraction ) / _samplesPerSegment;
}
public Vector3 PositionAtDistance( float distance )
{
return _curve.GetPositionByPointIndex( PointIndexAtDistance( distance ) );
}
/** The curve's direction at a distance, as a central difference over
`delta` meters. Falls back to a forward or backward difference at the
ends, and to zero on a degenerate curve.
*/
public Vector3 DirectionAtDistance( float distance, float delta )
{
var total = length;
if ( total <= 0.000001f )
{
return Vector3.Zero;
}
var step = Mathf.Min( Mathf.Max( delta, 0.0001f ), total * 0.5f );
var before = Mathf.Max( 0, distance - step );
var after = Mathf.Min( total, distance + step );
var direction = PositionAtDistance( after ) - PositionAtDistance( before );
return direction.LengthSquared() < 0.000000001f ? Vector3.Zero : direction.Normalized();
}
}
}