Editor/Geometry/ArchMeshSweep.Ribs.cs

Geometry helper for swept meshes. Computes rib positions across rows given developed section grid and carve volumes, finding where carve footprints cross row gaps and producing either plain or split rib layouts with per-row points and across offsets.

File Access
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// A swept strip with a rib standing wherever a cut's boundary crosses it, and the developed grid to match.
sealed class ArchMeshSweepRibs
{
	public List<Vector3[]> Rows { get; init; } = new();

	// Per row: a rib the SECTION put there quotes the one developed grid, a rib a CUT put there quotes its own
	// proportional place between the two it stands between - the same number in every row while the cut's edge
	// runs parallel to the road, raking with the edge where it does not.
	public List<float[]> Across { get; init; } = new();

	// Which SECTION gap each gap of the split grid came from. The brush list and the paving callback are both
	// indexed by the section's own columns, so a rib inserted between two of them would shift every brush after it.
	public int[] Gap { get; init; } = Array.Empty<int>();

	public int Columns => Rows.Count == 0 ? 0 : Rows[0].Length - 1;
}

// A section's ribs are where the ROAD wanted them; a cut's edge falls where it was dragged. Between the two the
// cut used to lose - a cell went whole or stayed whole - so a strip ONE column wide took nothing at all however
// much of its width a cut covered, which is why a cross-passage left the invert under a bore standing across it.
static partial class ArchMeshSweep
{
	// Slot COUNT is fixed per section gap at the most any row needs, so every row still comes out the same width
	// and the grid can never twist. A row needing fewer stands its spare ribs on the gap's far end; the empty
	// cells that leaves cost nothing, because ArchMesh reads a collapsed quad as a line and drops it.
	internal static ArchMeshSweepRibs Ribbed( IReadOnlyList<Vector3[]> rows, IReadOnlyList<ArchCarveVolume> volumes )
	{
		var section = Developed( rows );

		if ( volumes is not { Count: > 0 } )
		{
			return Plain( rows, section );
		}

		var gaps = rows[0].Length - 1;
		var crossings = new List<float>[rows.Count][];
		var slots = new int[gaps];

		for ( var row = 0; row < rows.Count; row++ )
		{
			crossings[row] = new List<float>[gaps];

			for ( var gap = 0; gap < gaps; gap++ )
			{
				var found = Crossed( rows[row][gap], rows[row][gap + 1], volumes );

				crossings[row][gap] = found;
				slots[gap] = Math.Max( slots[gap], found.Count );
			}
		}

		return Array.TrueForAll( slots, count => count == 0 )
			? Plain( rows, section )
			: Split( rows, section, crossings, slots, gaps );
	}

	// Where a cut's boundary crosses one gap of one row, as parameters along it. Only a cut whose BAND stands at
	// that height is asked - one passing clear over the strip wants no rib in it, and Carved would not take the
	// cell either way.
	static List<float> Crossed( Vector3 from, Vector3 to, IReadOnlyList<ArchCarveVolume> volumes )
	{
		var found = new List<float>();
		var near = new Vector2( from.x, from.y );
		var far = new Vector2( to.x, to.y );

		foreach ( var volume in volumes )
		{
			if ( volume.Footprint is not { Count: >= 3 } )
			{
				continue;
			}

			foreach ( var at in ArchFootprint.Crossings( volume.Footprint, near, far ) )
			{
				if ( Banded( volume, Vector3.Lerp( from, to, at ) ) )
				{
					found.Add( at );
				}
			}
		}

		found.Sort();

		return Spread( found, (to - from).Length );
	}

	// A rib a hair off one the section already stands, or off another cut's, is a sliver nobody can skin.
	static List<float> Spread( List<float> found, float length )
	{
		var kept = new List<float>();

		foreach ( var at in found )
		{
			if ( at * length < ArchGridService.FinestSize || (1f - at) * length < ArchGridService.FinestSize )
			{
				continue;
			}

			if ( kept.Count > 0 && (at - kept[^1]) * length < ArchGridService.FinestSize )
			{
				continue;
			}

			kept.Add( at );
		}

		return kept;
	}

	static ArchMeshSweepRibs Plain( IReadOnlyList<Vector3[]> rows, float[] section )
	{
		var ribs = new ArchMeshSweepRibs { Gap = Ordinals( rows[0].Length - 1 ) };

		foreach ( var row in rows )
		{
			ribs.Rows.Add( row );
			ribs.Across.Add( section );
		}

		return ribs;
	}

	static ArchMeshSweepRibs Split( IReadOnlyList<Vector3[]> rows, float[] section, List<float>[][] crossings, int[] slots, int gaps )
	{
		var mapping = new List<int>();

		for ( var gap = 0; gap < gaps; gap++ )
		{
			for ( var slot = 0; slot <= slots[gap]; slot++ )
			{
				mapping.Add( gap );
			}
		}

		var ribs = new ArchMeshSweepRibs { Gap = mapping.ToArray() };

		for ( var row = 0; row < rows.Count; row++ )
		{
			var points = new List<Vector3> { rows[row][0] };
			var across = new List<float> { section[0] };

			for ( var gap = 0; gap < gaps; gap++ )
			{
				var found = crossings[row][gap];

				for ( var slot = 0; slot < slots[gap]; slot++ )
				{
					var at = slot < found.Count ? found[slot] : 1f;

					points.Add( Vector3.Lerp( rows[row][gap], rows[row][gap + 1], at ) );
					across.Add( section[gap] + (section[gap + 1] - section[gap]) * at );
				}

				points.Add( rows[row][gap + 1] );
				across.Add( section[gap + 1] );
			}

			ribs.Rows.Add( points.ToArray() );
			ribs.Across.Add( across.ToArray() );
		}

		return ribs;
	}

	static int[] Ordinals( int count )
	{
		var ordinals = new int[count];

		for ( var index = 0; index < count; index++ )
		{
			ordinals[index] = index;
		}

		return ordinals;
	}
}