Editor/Road/ArchRoadGen.cs

Editor-side static generator for road geometry. It computes frames along a road curve, resolves bridges, tunnels and junction spans, and emits mesh-building calls for carriageways, pavements, markings, barriers, bridge and tunnel parts into an ArchOutputService.

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

namespace Sunless.Architecture;

public static class ArchRoadGen
{
	public static void Build( ArchOutputService output, string path, ArchRoadPart road, ArchPlan plan, ArchKit kit, ArchStyle style, IReadOnlyList<ArchJunctionNode> junctions, ArchGround ground )
	{
		var curve = road.Curve();

		if ( !curve.IsUsable )
		{
			return;
		}

		var frames = Frames( road, curve );

		if ( frames.Count < 2 )
		{
			return;
		}

		var crossings = ArchCrossover.Crossings( road, plan, curve );
		// Resolved first: the resolved span, not the drag's numbers, is where the structure actually is.
		var bridges = ArchBridge.Shapes( road, plan, curve, Frames( road, curve, crossings, kit ), kit, ground );
		var tunnels = ArchTunnel.Shapes( road, curve, Frames( road, curve, crossings, kit ), kit, ground, plan );
		// A road stops at every junction it reaches; the junction fills what lies between the spans.
		var spans = ArchJunction.Spans( road, junctions, curve.Length );
		var carves = Carves( plan, road, kit );

		for ( var index = 0; index < spans.Count; index++ )
		{
			var span = spans[index];
			var suffix = spans.Count > 1 ? $"_{index + 1}" : "";
			var swept = ArchJunction.Clipped( curve, frames, span );

			if ( swept.Count < 2 )
			{
				continue;
			}

			// A junction-cut span has two ends, so only an untouched loop still closes.
			var loop = road.Closed && span.Length >= curve.Length - 1f;

			output.Add( $"{path}/{ArchPieces.Carriageway}{suffix}", Transform.Zero, canvas => Carriageway( canvas, road, bridges, swept, style, loop, carves ) );

			foreach ( var right in Sides( road.Pavements ) )
			{
				output.Add( $"{path}/{ArchPieces.Pavement}/{ArchPieces.Side( right )}{suffix}", Transform.Zero,
					canvas => ArchRoadKerb.Build( canvas, road, bridges, curve, crossings, right, kit, style, span, loop, carves ) );
			}

			if ( road.Markings )
			{
				output.Add( $"{path}/{ArchPieces.Markings}{suffix}", Transform.Zero,
					canvas => ArchRoadLines.Build( canvas, road, curve, crossings, kit, style, span, carves ) );
			}
		}

		foreach ( var right in Sides( road.Barriers ) )
		{
			output.Add( $"{path}/{ArchPieces.Barrier}/{ArchPieces.Side( right )}", Transform.Zero, canvas => Barrier( canvas, road, bridges, tunnels, curve, right, kit, style ) );
		}

		// The carriageway already swept the deck; here is only what hangs below it and stands on it.
		foreach ( var bridge in bridges )
		{
			var name = ArchNames.Bridge( bridge.Part );
			var shape = bridge;

			output.Add( $"{path}/{name}/{ArchPieces.Girder}", Transform.Zero, canvas => ArchBridgeGen.Girder( canvas, shape, kit, style ) );
			output.Add( $"{path}/{name}/{ArchPieces.Bents}", Transform.Zero, canvas => ArchBridgeGen.Bents( canvas, shape, kit, style ) );

			if ( shape.Part.Abutments )
			{
				output.Add( $"{path}/{name}/{ArchPieces.Abutments}", Transform.Zero, canvas => ArchBridgeGen.Abutments( canvas, shape, kit, style ) );
			}

			foreach ( var right in new[] { false, true } )
			{
				var side = right;

				output.Add( $"{path}/{name}/{ArchPieces.Upstand}/{ArchPieces.Side( side )}", Transform.Zero,
					canvas => ArchBridgeGen.Upstand( canvas, shape, side, kit, style ) );

				if ( !shape.Part.Parapet )
				{
					continue;
				}

				output.Add( $"{path}/{name}/{ArchPieces.Parapet}/{ArchPieces.Side( side )}", Transform.Zero,
					canvas => ArchBridgeGen.Parapet( canvas, shape, side, curve, kit, style ) );
			}
		}

		// The road runs through the bore on the ground; here is only the bore around it and the portals.
		foreach ( var tunnel in tunnels )
		{
			var name = ArchNames.Tunnel( tunnel.Part );
			var shape = tunnel;

			output.Add( $"{path}/{name}/{ArchPieces.Lining}", Transform.Zero, canvas => ArchTunnelGen.Lining( canvas, shape, style ) );
			output.Add( $"{path}/{name}/{ArchPieces.Invert}", Transform.Zero, canvas => ArchTunnelGen.Invert( canvas, shape, kit, style ) );

			foreach ( var right in new[] { false, true } )
			{
				var side = right;

				if ( !ArchTunnelGen.Walked( road, shape.Part, side ) )
				{
					continue;
				}

				output.Add( $"{path}/{name}/{ArchPieces.Walkway}/{ArchPieces.Side( side )}", Transform.Zero,
					canvas => ArchTunnelGen.Walkway( canvas, shape, side, kit, style ) );
			}

			if ( !shape.Part.Portals )
			{
				continue;
			}

			output.Add( $"{path}/{name}/{ArchPieces.Portal}/{ArchPieces.Near}", Transform.Zero, canvas => ArchTunnelPortal.Build( canvas, shape, shape.Near, kit, style ) );
			output.Add( $"{path}/{name}/{ArchPieces.Portal}/{ArchPieces.Far}", Transform.Zero, canvas => ArchTunnelPortal.Build( canvas, shape, shape.Far, kit, style ) );
		}
	}

	// The cuts standing over this road, resolved ONCE for every strip it lays: a hole in the street is world-space
	// like every other, so the carriageway and the pavement ask ArchCut what covers them exactly as a slab does.
	// Scoped to the road as its own host, and ordered against it, so a cut made before the road was laid leaves it
	// alone. A road stands on no storey, which is why the level asked for is the ground's.
	static List<ArchCarveVolume> Carves( ArchPlan plan, ArchRoadPart road, ArchKit kit )
	{
		if ( plan is null )
		{
			return new List<ArchCarveVolume>();
		}

		return ArchCut.Volumes( plan, 0, kit, float.MinValue * 0.5f, float.MaxValue * 0.5f,
			road.Id, ArchCutAffects.Roadway, road.Id ).ToList();
	}

	public static List<ArchFrame> Frames( ArchRoadPart road, ArchCurve curve )
	{
		return curve.Walk( road.Precision, road.Simplify, road.StraightThreshold, road.MinimumStraightRun,
			ArchBridge.Stations( road ).Concat( ArchTunnel.Stations( road, curve ) ) );
	}

	// Pins crossover edges and bridge ends: simplification collapses exactly the frames those features are described at.
	public static List<ArchFrame> Frames( ArchRoadPart road, ArchCurve curve, IReadOnlyList<ArchRoadCrossing> crossings, ArchKit kit )
	{
		return curve.Walk( road.Precision, road.Simplify, road.StraightThreshold, road.MinimumStraightRun,
			ArchCrossover.Stations( road, crossings, kit )
				.Concat( ArchBridge.Stations( road ) )
				.Concat( ArchTunnel.Stations( road, curve ) ) );
	}

	public static IEnumerable<bool> Sides( RoadSide setting )
	{
		if ( setting is RoadSide.Left or RoadSide.Both )
		{
			yield return false;
		}

		if ( setting is RoadSide.Right or RoadSide.Both )
		{
			yield return true;
		}
	}

	// Left edge, crown, right edge: the camber crease is a real edge loop, not a shading trick.
	static void Carriageway(
		ArchMesh canvas,
		ArchRoadPart road,
		IReadOnlyList<ArchBridgeShape> bridges,
		IReadOnlyList<ArchFrame> frames,
		ArchStyle style,
		bool closed,
		IReadOnlyList<ArchCarveVolume> carves )
	{
		var brush = style.Brush( ArchSurface.Road, road.Palette );
		var camber = MathF.Max( 0f, road.Camber );
		var rows = new List<Vector3[]>();

		foreach ( var frame in frames )
		{
			var half = road.HalfWidth * frame.WidthScale;

			rows.Add( new[]
			{
				frame.Side( -half, -camber ),
				frame.Position,
				frame.Side( half, -camber )
			} );
		}

		ArchMeshSweep.Solid( canvas, rows, frames, road.Thickness, new[] { brush }, brush, closed,
			soffit: row => ArchBridge.Soffit( bridges, road, frames[row] ), carves: carves );
	}

	// Gives up the stretch a bridge parapet takes over, and any stretch inside a bore.
	static void Barrier(
		ArchMesh canvas,
		ArchRoadPart road,
		IReadOnlyList<ArchBridgeShape> bridges,
		IReadOnlyList<ArchTunnelShape> tunnels,
		ArchCurve curve,
		bool right,
		ArchKit kit,
		ArchStyle style )
	{
		var stand = Verge( road, right );
		var lift = road.Carries( road.Pavements, right ) ? road.KerbHeight : 0f;
		var shape = ArchBarrierShape.Resolve( curve, road.Barrier, right ? stand : -stand, lift, kit );

		ArchBarrierGen.Build( canvas, shape, road.Barrier, kit, ArchBarrierBrushes.Of( style, road.Palette ),
			distance => Parapeted( bridges, distance ) || Lined( tunnels, distance ) );
	}

	// On the RESOLVED span: the two railings meet at the abutment, neither stretch left bare.
	static bool Parapeted( IReadOnlyList<ArchBridgeShape> bridges, float distance )
	{
		foreach ( var bridge in bridges )
		{
			if ( bridge.Part.Parapet && bridge.Carries( distance ) )
			{
				return true;
			}
		}

		return false;
	}

	static bool Lined( IReadOnlyList<ArchTunnelShape> tunnels, float distance )
	{
		foreach ( var tunnel in tunnels )
		{
			if ( tunnel.Carries( distance ) )
			{
				return true;
			}
		}

		return false;
	}

	// Where a barrier stands: the outer edge of whatever carries it, pulled in by the inset.
	public static float Verge( ArchRoadPart road, bool right )
	{
		return MathF.Max( 4f, road.Reach( right ) - MathF.Max( 0f, road.BarrierInset ) );
	}
}