Editor/Road/ArchRoadKerb.cs

Editor-side static helper that generates the kerb (road verge) mesh for roads. It computes frame rows, paving cells, brushes and vertex rows then calls mesh-sweep routines to build kerb geometry for straight and corner sections.

Native Interop
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// Verge section, road edge outward; the pavement field is loop-cut into cells so it lays as paving stones.
public static class ArchRoadKerb
{
	// Ribs 0-4 are the kerb stone; the 4-5 gap IS the paving, one edge loop, collapsed where no crossover reaches.
	const int Field = 4;

	public static void Build(
		ArchMesh canvas,
		ArchRoadPart road,
		IReadOnlyList<ArchBridgeShape> bridges,
		ArchCurve curve,
		IReadOnlyList<ArchRoadCrossing> crossings,
		bool right,
		ArchKit kit,
		ArchStyle style,
		ArchRoadSpan span,
		bool closed,
		IReadOnlyList<ArchCarveVolume> carves = null )
	{
		if ( road.PavementWidth < 6f )
		{
			return;
		}

		var frames = ArchJunction.Clipped( curve, Frames( road, curve, crossings, kit ), span );

		Sweep( canvas, road, bridges, frames, crossings, right, kit, style, closed, true, carves );
	}

	// The corner path IS the kerb line, so the section starts on the frame with no centreline offset.
	// Frames cannot say zero: ArchCurve reads a zero width scale as 1.
	public static void Corner( ArchMesh canvas, ArchRoadPart road, IReadOnlyList<ArchFrame> frames, bool right, ArchKit kit, ArchStyle style )
	{
		if ( road.PavementWidth < 6f )
		{
			return;
		}

		Sweep( canvas, road, Array.Empty<ArchBridgeShape>(), frames, Array.Empty<ArchRoadCrossing>(), right, kit, style, false, false );
	}

	static void Sweep(
		ArchMesh canvas,
		ArchRoadPart road,
		IReadOnlyList<ArchBridgeShape> bridges,
		IReadOnlyList<ArchFrame> frames,
		IReadOnlyList<ArchRoadCrossing> crossings,
		bool right,
		ArchKit kit,
		ArchStyle style,
		bool closed,
		bool centreline,
		IReadOnlyList<ArchCarveVolume> carves = null )
	{
		if ( frames.Count < 2 )
		{
			return;
		}

		var paved = ArchCrossover.Paved( road, crossings, right );
		var cells = Cells( road );
		var rows = new List<Vector3[]>();

		foreach ( var frame in frames )
		{
			var edge = centreline ? road.HalfWidth * frame.WidthScale : 0f;

			rows.Add( Section( road, frame, crossings, paved, right, cells, kit, edge ) );
		}

		// Only centreline runs take the deck's soffit: a corner's frames misread as road distances.
		var soffit = centreline ? ( Func<int, float?> )(row => ArchBridge.Soffit( bridges, road, frames[row] )) : null;

		ArchMeshSweep.Solid( canvas, rows, frames, road.Thickness + road.KerbHeight,
			Brushes( road, cells, right, style ), style.Brush( ArchSurface.Kerb, road.Palette ), closed,
			Paving( road, paved, cells, right, style ), soffit, carves );
	}

	// Paving walks its own frames at the paver pitch, never simplified; off, the whole verge follows the road's frames.
	static List<ArchFrame> Frames( ArchRoadPart road, ArchCurve curve, IReadOnlyList<ArchRoadCrossing> crossings, ArchKit kit )
	{
		if ( road.PaverSize < 2f )
		{
			return ArchRoadGen.Frames( road, curve, crossings, kit );
		}

		return curve.Walk( MathF.Min( road.Precision, road.PaverSize ), false, 1f, 3,
			ArchCrossover.Stations( road, crossings, kit ) );
	}

	// The crossover is one column between stone and splay, no per-face test; null when nothing is paved.
	static Func<int, int, ArchBrush?> Paving( ArchRoadPart road, IReadOnlyList<ArchRoadCrossing> paved, int cells, bool right, ArchStyle style )
	{
		if ( paved.Count == 0 )
		{
			return null;
		}

		var brush = style.Brush( ArchSurface.Deck, road.Palette );
		// Left section reads outward in: the paving column lands the same count in from the far end.
		var column = right ? Field : cells;

		return ( _, gap ) => gap == column ? brush : null;
	}

	public static int Cells( ArchRoadPart road )
	{
		if ( road.PaverSize >= 2f )
		{
			return ArchDivide.AtMost( FieldWidth( road ), road.PaverSize ).Count;
		}

		return Math.Max( 1, road.PavementCourses );
	}

	public static float FieldWidth( ArchRoadPart road )
	{
		return MathF.Max( 2f, Width( road ) - Stone( road ) );
	}

	static float Stone( ArchRoadPart road ) => MathF.Max( 0f, road.KerbWidth );

	static List<ArchBrush> Brushes( ArchRoadPart road, int cells, bool right, ArchStyle style )
	{
		var kerb = style.Brush( ArchSurface.Kerb, road.Palette );
		var pavement = style.Brush( ArchSurface.Pavement, road.Palette );
		var gaps = new List<ArchBrush>();

		// One gap more than cells: the crossover's column takes pavement material where no drive paves it.
		for ( var gap = 0; gap <= Field + cells; gap++ )
		{
			gaps.Add( gap < Field ? kerb : pavement );
		}

		// The left section is read outward in, so its brushes are too.
		if ( !right )
		{
			gaps.Reverse();
		}

		return gaps;
	}

	static Vector3[] Section(
		ArchRoadPart road,
		ArchFrame frame,
		IReadOnlyList<ArchRoadCrossing> crossings,
		IReadOnlyList<ArchRoadCrossing> paved,
		bool right,
		int cells,
		ArchKit kit,
		float edge )
	{
		var camber = -MathF.Max( 0f, road.Camber );
		var top = camber + ArchCrossover.KerbTop( road, crossings, right, frame.Distance, kit );
		var back = ArchCrossover.Back( road, kit );
		var bevel = Bevel( road, kit );
		var rise = top - camber;
		var stone = Stone( road );

		// The foot starts a bite inside the carriageway, or the kerb's back and the road's flank sit back to back.
		var foot = -ArchContact.Bite( kit );

		var ribs = new List<Vector2>
		{
			new( foot, camber ),
			road.Kerb == KerbStyle.Rolled ? new Vector2( bevel * 0.15f, camber + rise * 0.5f ) : new Vector2( foot, camber ),
			road.Kerb == KerbStyle.Rolled ? new Vector2( bevel * 0.55f, camber + rise * 0.86f ) : new Vector2( foot, camber ),
			new( bevel, top ),
			new( bevel + stone, top )
		};

		var field = FieldWidth( road );
		// Measured from the back of the STONE: the first field rib lands on it and the splay can collapse; else sliver quads.
		float Raked( float across ) => MathX.Lerp( top, back, Math.Clamp( (across - bevel) / MathF.Max( 1f, field - bevel ), 0f, 1f ) );

		// The splay is one rib; courses held off it collapse at the mouth's deepest and reappear as it tapers.
		var splay = MathF.Max( bevel, ArchCrossover.Depth( paved, frame.Distance, field ) );

		ribs.Add( new Vector2( stone + splay, Raked( splay ) ) );

		foreach ( var (_, across) in ArchDivide.AtMost( field, field / MathF.Max( 1, cells ) ).Bays )
		{
			var held = MathF.Max( across, splay );

			ribs.Add( new Vector2( stone + held, Raked( held ) ) );
		}

		return Row( frame, edge, ribs, right );
	}

	static Vector3[] Row( ArchFrame frame, float edge, IReadOnlyList<Vector2> ribs, bool right )
	{
		var row = new Vector3[ribs.Count];

		for ( var index = 0; index < ribs.Count; index++ )
		{
			var across = edge + ribs[index].x;

			row[index] = frame.Side( right ? across : -across, ribs[index].y );
		}

		// Columns always run left to right across the run, so a left-hand section is read outward in.
		if ( !right )
		{
			Array.Reverse( row );
		}

		return row;
	}

	static float Width( ArchRoadPart road ) => MathF.Max( 6f, road.PavementWidth );

	static float Bevel( ArchRoadPart road, ArchKit kit )
	{
		return road.Kerb switch
		{
			KerbStyle.Square => 0f,
			KerbStyle.Rolled => MathF.Max( road.KerbBevel, road.KerbHeight * 1.2f ),
			_ => MathF.Max( 0f, road.KerbBevel )
		};
	}
}