Utility for computing row positions along a road curve. It can produce evenly spaced rows or a simplified subset based on angle, deviation and max gap, using cross-section points to test collapsible rows.
using System;
using System.Collections.Generic;
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// Whether a road type's geometry is thinned out where the road hardly bends, and by how much: a row is dropped
/// while the road turns less than <see cref="Angle"/> there and dropping it moves the road no further than
/// <see cref="Deviation"/> from its true curve. A straight street then costs two rows instead of dozens; a road
/// whose shape matters at every row (a country verge wandering with noise) keeps them all.
/// </summary>
/// <param name="MaxGap">
/// How far apart the rows may be spread at the most, whatever the two measures allow; 0 for no limit. It is what a
/// road type whose border has shape of its own between the rows (a verge wandering with noise) uses to stop the
/// thinning running away with detail the sampler cannot see.
/// </param>
public readonly record struct RoadSimplify(bool Enabled, float Angle, float Deviation, float MaxGap = 0.0f)
{
public void AddToHash(ref StableHash _Hash)
{
_Hash.Add(Enabled ? 1 : 0);
if (!Enabled)
return;
_Hash.Add(Angle);
_Hash.Add(Deviation);
_Hash.Add(MaxGap);
}
}
/// <summary>
/// Where the rows of a stretch of road sit along its curve. Every builder of a chunk (surface, borders, bridge)
/// asks for the same rows, so the pieces meet exactly; the ends are always the stretch's own distances, which is
/// what the junctions on either side meet.
/// </summary>
internal static class RoadRows
{
/// <summary>
/// Rows every spacing along a stretch, its exact ends included.
/// </summary>
public static List<float> Even(float _Start, float _End, float _Spacing)
{
float length = MathF.Max(0.0f, _End - _Start);
int steps = Math.Max(1, (int)MathF.Ceiling(length / MathF.Max(1.0f, _Spacing)));
var rows = new List<float>(steps + 1);
for (int i = 0; i <= steps; i++)
rows.Add(i == steps ? _End : _Start + length * i / steps);
return rows;
}
/// <summary>
/// The rows a stretch of road is built on. The road's two edges are what is measured, not its centreline: an
/// edge that moves out on its own (a lane appearing over a taper) has to keep its rows even where the
/// centreline runs dead straight.
/// </summary>
public static List<float> Build(RoadCurveTable _Table, float _Start, float _End, float _Spacing, in RoadWidth _Width, in RoadSimplify _Simplify)
{
var rows = Even(_Start, _End, _Spacing);
if (!_Simplify.Enabled || rows.Count < 3)
return rows;
var left = new Vector3[rows.Count];
var right = new Vector3[rows.Count];
for (int i = 0; i < rows.Count; i++)
RoadSurfaceBuilder.CrossSection(_Table, rows[i], _Width, 0.0f, out left[i], out right[i]);
var kept = Walk(rows, _Simplify, left, right);
var distances = new List<float>(kept.Count);
foreach (int index in kept)
distances.Add(rows[index]);
return distances;
}
/// <summary>
/// Walks the rows from one end, each time reaching as far ahead as the rows in between can be dropped for on
/// every track measured, and never further than the road type lets its rows be spread.
/// </summary>
private static List<int> Walk(IReadOnlyList<float> _Rows, in RoadSimplify _Simplify, params Vector3[][] _Tracks)
{
int last = _Rows.Count - 1;
var kept = new List<int>(_Rows.Count) { 0 };
int anchor = 0;
while (anchor < last)
{
int next = anchor + 1;
for (int candidate = anchor + 2; candidate <= last; candidate++)
{
// Two rows are never merged into one, however straight the road is, so the cap can only ever
// stop the reach growing — the rows already there all stay.
if (_Simplify.MaxGap > 0.0f && _Rows[candidate] - _Rows[anchor] > _Simplify.MaxGap)
break;
bool droppable = true;
foreach (var track in _Tracks)
droppable &= CanDrop(track, anchor, candidate, _Simplify);
if (!droppable)
break;
next = candidate;
}
kept.Add(next);
anchor = next;
}
return kept;
}
/// <summary>
/// Whether every row between two others can go, as far as one edge is concerned: the edge has to turn less
/// than the angle at each of them, and none of them may end up further than the deviation from where the edge
/// would run without them.
/// </summary>
private static bool CanDrop(Vector3[] _Points, int _From, int _To, in RoadSimplify _Simplify)
{
Vector3 chord = _Points[_To] - _Points[_From];
float chordLengthSquared = chord.LengthSquared;
for (int k = _From + 1; k < _To; k++)
{
if (TurnAngle(_Points, k) >= _Simplify.Angle)
return false;
if (_Simplify.Deviation <= 0.0f)
continue;
Vector3 offset = _Points[k] - _Points[_From];
float t = chordLengthSquared > 0.0f ? Math.Clamp(Vector3.Dot(offset, chord) / chordLengthSquared, 0.0f, 1.0f) : 0.0f;
if ((offset - chord * t).Length > _Simplify.Deviation)
return false;
}
return true;
}
/// <summary>
/// How far the road turns at a row, in degrees.
/// </summary>
private static float TurnAngle(Vector3[] _Points, int _Index)
{
Vector3 incoming = _Points[_Index] - _Points[_Index - 1];
Vector3 outgoing = _Points[_Index + 1] - _Points[_Index];
if (incoming.IsNearZeroLength || outgoing.IsNearZeroLength)
return 0.0f;
return MathF.Acos(Math.Clamp(Vector3.Dot(incoming.Normal, outgoing.Normal), -1.0f, 1.0f)).RadianToDegree();
}
}