Utility class that builds an arc-length sample table for a WaterCurve. It precomputes sample positions and cumulative distances, exposes length, bounds, sampling by index or distance, and provides position/tangent lookups and a frame method.
using System;
using Sandbox;
namespace RedSnail.WaterTool2;
/// <summary>
/// Arc-length table over a <see cref="WaterCurve"/>, so a river can be sampled at even distances (mesh rows) instead
/// of at even parameter values, which bunch up around tight bends. The samples double as a polyline for picking.
/// </summary>
public sealed class WaterCurveTable
{
private readonly Vector3[] m_Positions;
private readonly float[] m_Distances;
public WaterCurve Curve { get; }
public float Length => m_Distances[^1];
public int SampleCount => m_Positions.Length;
/// <summary>
/// The box the curve itself runs through.
/// </summary>
public BBox Bounds { get; }
public WaterCurveTable(WaterCurve _Curve, int _Samples)
{
Curve = _Curve;
int count = Math.Max(1, _Samples);
m_Positions = new Vector3[count + 1];
m_Distances = new float[count + 1];
m_Positions[0] = _Curve.P0;
var bounds = new BBox(_Curve.P0, _Curve.P0);
for (int i = 1; i <= count; i++)
{
m_Positions[i] = _Curve.Evaluate((float)i / count);
m_Distances[i] = m_Distances[i - 1] + Vector3.DistanceBetween(m_Positions[i - 1], m_Positions[i]);
bounds = bounds.AddPoint(m_Positions[i]);
}
Bounds = bounds;
}
/// <summary>
/// Builds a table with a sample density that suits the curve's length.
/// </summary>
public static WaterCurveTable Create(WaterCurve _Curve)
{
int samples = Math.Clamp((int)(_Curve.ApproximateLength() / 20.0f), 16, 1024);
return new WaterCurveTable(_Curve, samples);
}
public Vector3 SamplePosition(int _Index)
{
return m_Positions[_Index];
}
public float SampleDistance(int _Index)
{
return m_Distances[_Index];
}
public float DistanceToT(float _Distance)
{
if (_Distance <= 0.0f)
return 0.0f;
if (_Distance >= Length)
return 1.0f;
int low = 0;
int high = m_Distances.Length - 1;
while (high - low > 1)
{
int mid = (low + high) / 2;
if (m_Distances[mid] <= _Distance)
low = mid;
else
high = mid;
}
float span = m_Distances[high] - m_Distances[low];
float fraction = span > 0.0f ? (_Distance - m_Distances[low]) / span : 0.0f;
return (low + fraction) / (m_Distances.Length - 1);
}
public Vector3 PositionAt(float _Distance)
{
return Curve.Evaluate(DistanceToT(_Distance));
}
public Vector3 TangentAt(float _Distance)
{
return Curve.Tangent(DistanceToT(_Distance));
}
/// <summary>
/// Where the river is at a distance and which way it runs there, in one go.
/// </summary>
public void Frame(float _Distance, out Vector3 _Position, out Vector3 _Tangent)
{
float t = DistanceToT(_Distance);
_Position = Curve.Evaluate(t);
_Tangent = Curve.Tangent(t);
}
}