Editor/Road/ArchJunctionGen.cs

Editor code that generates the geometry for a road junction apron and kerbs. It builds an apron mesh by creating concentric rings toward a crowned center, draws bands and wedges between rings, and generates kerb/verge geometry for junction corners using road kerb helpers. Also provides ArchRoadSheet to map world points to road sheet UV/station coordinates.

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

namespace Sunless.Architecture;

// Corner reuses the road's own kerb section, so its heights cannot disagree with the legs.
public static class ArchJunctionGen
{
	const float Step = 36f;
	const int Rings = 3;

	public static void Build( ArchOutputService output, string path, ArchJunctionNode node, ArchKit kit, ArchStyle style )
	{
		if ( node.Legs.Count < 3 )
		{
			return;
		}

		output.Add( $"{path}/Apron", Transform.Zero, canvas => Apron( canvas, node, kit, style ) );

		for ( var index = 0; index < node.Corners.Count; index++ )
		{
			var corner = node.Corners[index];

			output.Add( $"{path}/Kerb_{index + 1}", Transform.Zero, canvas => Verge( canvas, corner, kit, style ) );
		}
	}

	// Pavement on the right: a corner runs one leg's left edge to the next's right.
	static void Verge( ArchMesh canvas, ArchJunctionCorner corner, ArchKit kit, ArchStyle style )
	{
		var road = corner.From.Road;

		if ( road.Pavements == RoadSide.None || road.PavementWidth < 6f )
		{
			return;
		}

		// Corner turns in a few hundred inches, so its walk is far finer than a road's.
		var frames = corner.Path.Walk( Math.Clamp( corner.Path.Length / 12f, 8f, Step ) );

		if ( frames.Count >= 2 )
		{
			ArchRoadKerb.Corner( canvas, road, frames, true, kit, style );
		}
	}

	// Rings closing on the crown, not one flat plate that cannot carry a tiled surface.
	static void Apron( ArchMesh canvas, ArchJunctionNode node, ArchKit kit, ArchStyle style )
	{
		var loop = node.Boundary( Step );

		if ( loop.Count < 3 )
		{
			return;
		}

		var lead = node.Legs[0].Road;
		var brush = style.Brush( ArchSurface.Road, lead.Palette );
		var crown = Crown( loop, lead );
		var sheet = new ArchRoadSheet( node.Legs[0] );
		var rings = new List<List<Vector3>> { loop };

		for ( var ring = 1; ring < Rings; ring++ )
		{
			var reach = ring / (float)Rings;

			rings.Add( loop.Select( point => Vector3.Lerp( point, crown, reach ) ).ToList() );
		}

		for ( var ring = 0; ring < rings.Count - 1; ring++ )
		{
			Band( canvas, rings[ring], rings[ring + 1], sheet, brush );
		}

		Fan( canvas, rings[^1], crown, sheet, brush );

		// A slab, not a skin: beside solid legs a thin surface reads as a hole.
		var floor = loop.Min( point => point.z ) - MathF.Max( 1f, lead.Thickness );
		var under = loop.Select( point => point.WithZ( floor ) ).ToList();

		canvas.Prism( under, loop, brush, false );
		canvas.Polygon( under, brush, true );
	}

	static void Band( ArchMesh canvas, IReadOnlyList<Vector3> outer, IReadOnlyList<Vector3> inner, ArchRoadSheet sheet, ArchBrush brush )
	{
		for ( var index = 0; index < outer.Count; index++ )
		{
			var next = (index + 1) % outer.Count;

			canvas.Ribbon( outer[index], outer[next], inner[next], inner[index], sheet.Along, brush, new ArchWeave
			{
				A = sheet.At( outer[index] ),
				B = sheet.At( outer[next] ),
				C = sheet.At( inner[next] ),
				D = sheet.At( inner[index] )
			} );
		}
	}

	static void Fan( ArchMesh canvas, IReadOnlyList<Vector3> ring, Vector3 crown, ArchRoadSheet sheet, ArchBrush brush )
	{
		for ( var index = 0; index < ring.Count; index++ )
		{
			var next = (index + 1) % ring.Count;

			canvas.Wedge( ring[index], ring[next], crown, sheet.Along, brush, new ArchWeave
			{
				A = sheet.At( ring[index] ),
				B = sheet.At( ring[next] ),
				C = sheet.At( crown ),
				D = sheet.At( crown )
			} );
		}
	}

	// Crowned like the roads, so water runs off and a leg's crown meets no dish.
	static Vector3 Crown( IReadOnlyList<Vector3> loop, ArchRoadPart lead )
	{
		var centre = Vector3.Zero;

		foreach ( var point in loop )
		{
			centre += point;
		}

		return centre / loop.Count + Vector3.Up * MathF.Max( 0f, lead.Camber );
	}
}

// One leg's developed surface, so the apron shares the road's grid, not a world projection.
public sealed class ArchRoadSheet
{
	readonly Vector3 origin;
	readonly Vector3 along;
	readonly Vector3 across;
	readonly float station;
	readonly float offset;

	public ArchRoadSheet( ArchJunctionLeg leg )
	{
		var frame = leg.Frame();

		origin = frame.Position;
		along = frame.Along;
		across = frame.Across;
		station = leg.Cut;
		// The road unrolls from its LEFT edge, so the sheet must start there too.
		offset = leg.HalfWidth * MathF.Max( 0.01f, frame.WidthScale );
	}

	public Vector3 Along => along;

	public Vector2 At( Vector3 point )
	{
		var reach = point - origin;

		return new Vector2( station + Vector3.Dot( reach, along ), offset + Vector3.Dot( reach, across ) );
	}
}