Geometry/WaterCurveTable.cs

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.

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