Editor/Road/ArchRoadLines.cs
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

public static class ArchRoadLines {
	public static void Build(
		ArchMesh canvas,
		ArchRoadPart road,
		ArchCurve curve,
		IReadOnlyList<ArchRoadCrossing> crossings,
		ArchKit kit,
		ArchStyle style,
		ArchRoadSpan span,
		IReadOnlyList<ArchCarveVolume> carves = null ) {
		var brush = style.Brush( ArchSurface.RoadLine, road.Palette );
		// Proud, not a bite: a bite shares a plane with the camber crease.
		var lift = ArchLap.Proud( kit );

		Paint( canvas, road, curve, kit, kit.FindRoadLine( road.CentreLine ), 0f, lift, brush, span, carves );

		foreach ( var offset in LaneOffsets( road ) ) {
			Paint( canvas, road, curve, kit, kit.FindRoadLine( road.LaneLine ), offset, lift, brush, span, carves );
		}

		foreach ( var side in new[] { -1f, 1f } ) {
			Paint( canvas, road, curve, kit, kit.FindRoadLine( road.EdgeLine ), side * (road.HalfWidth - kit.EdgeLineInset), lift, brush, span, carves );
		}

		foreach ( var crossing in crossings ) {
			if ( crossing.Kind == CrossingKind.Zebra && crossing.Distance >= span.From && crossing.Distance <= span.To ) {
				Zebra( canvas, road, curve, crossing, kit, lift, brush, carves );
			}
		}
	}

	// Lane boundaries only - the centre is drawn by its own line, which is usually a different one.
	static IEnumerable<float> LaneOffsets( ArchRoadPart road ) {
		var lanes = Math.Clamp( road.Lanes, 1, 12 );

		if ( lanes < 3 ) {
			yield break;
		}

		var step = road.Width / lanes;

		for ( var index = 1; index < lanes; index++ ) {
			var offset = -road.HalfWidth + step * index;

			if ( MathF.Abs( offset ) > 0.5f ) {
				yield return offset;
			}
		}
	}

	static void Paint( ArchMesh canvas, ArchRoadPart road, ArchCurve curve, ArchKit kit, ArchRoadLine line, float offset, float lift, ArchBrush brush, ArchRoadSpan span, IReadOnlyList<ArchCarveVolume> carves ) {
		if ( line is null || line.Width < 0.2f ) {
			return;
		}

		var strands = Math.Clamp( line.Count, 1, 4 );
		var spread = (strands - 1) * (line.Width + line.Separation) * 0.5f;

		for ( var strand = 0; strand < strands; strand++ ) {
			var centre = offset - spread + strand * (line.Width + line.Separation);

			foreach ( var run in Runs( span, line ) ) {
				Strip( canvas, road, curve, kit, centre, line.Width, lift, run.From, run.To, brush, carves );
			}
		}
	}

	// Dash phase from the road's own start, not the span's: marks do not shift where a junction interrupts them.
	static IEnumerable<(float From, float To)> Runs( ArchRoadSpan span, ArchRoadLine line ) {
		if ( line.Dash < 1f || line.Gap < 0.5f ) {
			yield return (span.From, span.To);
			yield break;
		}

		var pitch = line.Dash + line.Gap;
		var at = line.Gap * 0.5f;

		while ( at + line.Dash <= span.To ) {
			if ( at >= span.From ) {
				yield return (at, at + line.Dash);
			}

			at += pitch;
		}
	}

	static void Strip( ArchMesh canvas, ArchRoadPart road, ArchCurve curve, ArchKit kit, float centre, float width, float lift, float from, float to, ArchBrush brush, IReadOnlyList<ArchCarveVolume> carves ) {
		var rows = new List<Vector3[]>();
		var frames = new List<ArchFrame>();
		var half = width * 0.5f;

		foreach ( var distance in ArchDivide.AtMost( to - from, road.Precision ).Nodes ) {
			if ( !curve.Sample( from + distance, out var frame ) ) {
				continue;
			}

			rows.Add( new[]
			{
				frame.Side( centre - half, Deck( road, centre - half ) + lift ),
				frame.Side( centre + half, Deck( road, centre + half ) + lift )
			} );

			frames.Add( frame );
		}

		ArchMeshSweep.Skin( canvas, rows, frames, brush, false, carves );
	}

	static void Zebra( ArchMesh canvas, ArchRoadPart road, ArchCurve curve, ArchRoadCrossing crossing, ArchKit kit, float lift, ArchBrush brush, IReadOnlyList<ArchCarveVolume> carves ) {
		var half = MathF.Max( 24f, crossing.Width ) * 0.5f;
		var bar = MathF.Max( 4f, kit.ZebraBar );
		var reach = road.HalfWidth - kit.EdgeLineInset;

		foreach ( var centre in ArchDivide.AtMost( half * 2f, bar + MathF.Max( 2f, kit.ZebraGap ) ).Centres ) {
			var rows = new List<Vector3[]>();
			var frames = new List<ArchFrame>();

			foreach ( var edge in new[] { -bar * 0.5f, bar * 0.5f } ) {
				if ( !curve.Sample( crossing.Distance - half + centre + edge, out var frame ) ) {
					continue;
				}

				rows.Add( new[]
				{
					frame.Side( -reach, Deck( road, -reach ) + lift ),
					frame.Side( reach, Deck( road, reach ) + lift )
				} );

				frames.Add( frame );
			}

			ArchMeshSweep.Skin( canvas, rows, frames, brush, false, carves );
		}
	}

	// A marking lies on the camber, not on the crown, or the outer half of every line hangs in the air.
	static float Deck( ArchRoadPart road, float across ) {
		return -MathF.Max( 0f, road.Camber ) * Math.Clamp( MathF.Abs( across ) / road.HalfWidth, 0f, 1f );
	}
}