Editor/LineProfile.cs
using System;
using System.Collections.Generic;
using Sandbox;
namespace TerrainLines;
// A distance-based height profile, independent of point density and the transverse shoulder.
internal sealed class LineProfile
{
private readonly double[] _heights;
private readonly float _length;
private readonly bool[] _roundJoins;
internal LineProfile( TerrainStorage storage, IReadOnlyList<Vector3> points, float length, float evenRamp = 0, float slopeLimit = 90 )
{
_length = length;
int intervals = (int)Math.Clamp( Math.Ceiling( length / (storage.TerrainSize / storage.Resolution) ), 8, 65536 );
_heights = new double[intervals + 1];
float travelled = 0;
int segment = 1;
float segmentLength = LineRaster.Distance2D( points[0], points[1] );
for ( int i = 0; i <= intervals; i++ )
{
float distance = length * i / intervals;
while ( segment < points.Count - 1 && travelled + segmentLength < distance )
{
travelled += segmentLength;
segment++;
segmentLength = LineRaster.Distance2D( points[segment - 1], points[segment] );
}
float t = segmentLength > 0 ? Math.Clamp( (distance - travelled) / segmentLength, 0, 1 ) : 0;
var position = points[segment - 1] + (points[segment] - points[segment - 1]) * t;
_heights[i] = LineRaster.SampleHeight( storage, position.x, position.y );
}
// Four implicit diffusion passes spread abrupt rises over roughly a tenth of the stroke.
// Fixed endpoints preserve arrival heights at this stage; linear grades are unchanged.
double lambda = Math.Pow( intervals * 0.1, 2 ) / 8;
var upper = new double[intervals];
var rhs = new double[intervals];
for ( int i = 1; i < intervals; i++ )
upper[i] = -lambda / (1 + 2 * lambda + lambda * upper[i - 1]);
for ( int pass = 0; pass < 4; pass++ )
{
for ( int i = 1; i < intervals; i++ )
{
double value = _heights[i];
if ( i == 1 ) value += lambda * _heights[0];
if ( i == intervals - 1 ) value += lambda * _heights[intervals];
rhs[i] = (value + lambda * rhs[i - 1]) / (1 + 2 * lambda + lambda * upper[i - 1]);
}
for ( int i = intervals - 1; i > 0; i-- )
_heights[i] = rhs[i] - (i < intervals - 1 ? upper[i] * _heights[i + 1] : 0);
}
double start = _heights[0], end = _heights[^1];
for ( int i = 0; i <= intervals; i++ )
{
double grade = start + (end - start) * i / intervals;
_heights[i] += (grade - _heights[i]) * Math.Clamp( evenRamp, 0, 1 );
}
if ( slopeLimit < 90 )
{
var original = (double[])_heights.Clone();
double rise = Math.Tan( Math.Clamp( slopeLimit, 0, 90 ) * Math.PI / 180 ) * length / intervals;
// The smallest raised envelope with this grade bound. Both sweeps are needed for hills and valleys.
for ( int i = 1; i <= intervals; i++ ) _heights[i] = Math.Max( _heights[i], _heights[i - 1] - rise );
for ( int i = intervals - 1; i >= 0; i-- ) _heights[i] = Math.Max( _heights[i], _heights[i + 1] - rise );
_roundJoins = new bool[intervals + 1];
for ( int i = 1; i < intervals; i++ )
_roundJoins[i] = _heights[i - 1] > original[i - 1] + 1e-8 || _heights[i] > original[i] + 1e-8 || _heights[i + 1] > original[i + 1] + 1e-8;
}
}
internal float Sample( float distance )
{
float t = Math.Clamp( distance / _length, 0, 1 );
float sample = t * (_heights.Length - 1);
int knot = (int)MathF.Round( sample );
if ( knot > 0 && knot < _heights.Length - 1 && (_roundJoins?[knot] ?? false) )
{
// Round only affected joins over one sample interval. The derivative is a convex blend
// of the two compliant grades, so rounding cannot introduce a steeper section.
double u = sample - knot + 0.5;
double a = (_heights[knot - 1] + _heights[knot]) * 0.5;
double b = _heights[knot];
double c = (_heights[knot] + _heights[knot + 1]) * 0.5;
return (float)((1 - u) * (1 - u) * a + 2 * (1 - u) * u * b + u * u * c);
}
int index = Math.Min( (int)sample, _heights.Length - 2 );
return (float)(_heights[index] + (_heights[index + 1] - _heights[index]) * (sample - index));
}
}