Editor/Road/ArchBridge.cs

Editor-side utility for bridge/arch placement in road editor. Computes bridge spans, soffit/girder/kerb dimensions, pier/bent placement, wing walls, sections and related geometry by sampling road curve, frames, ground and kit parameters.

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

namespace Sunless.Architecture;

// Where a road is carried on a structure - answered here and nowhere else, or four consumers drift.
public static class ArchBridge
{
	// Pier height with no terrain - skipped piers read as a deck floating on air.
	const float NominalRise = 168f;

	public const float ShortestSpan = 96f;

	// Below this a pier reads as a fin in the median - better moved than squeezed in.
	const float Narrowest = 18f;

	// A bent only decides which median it lands in - that is measured in feet.
	const float HuntStep = 12f;

	const float ProbeStep = 24f;

	// A clamp, not a search: past this the drag was describing something else.
	const float Trimmed = 0.4f;

	public static ArchBridgePart Carrying( ArchRoadPart road, float distance )
	{
		return new ArchRoadAttachmentService( road ).BridgeAt( distance );
	}

	public static bool Carried( ArchRoadPart road, float distance ) => Carrying( road, distance ) is not null;

	// The ONE soffit answer the carriageway and both verges take - a verge hangs a kerb's height lower.
	public static float? Soffit( ArchRoadPart road, ArchFrame frame )
	{
		var bridge = Carrying( road, frame.Distance );

		return bridge is null ? null : Soffit( road, bridge, frame );
	}

	// Read off the RESOLVED spans - their ends moved to the lip of what they cross.
	public static float? Soffit( IReadOnlyList<ArchBridgeShape> shapes, ArchRoadPart road, ArchFrame frame )
	{
		foreach ( var shape in shapes )
		{
			if ( shape.Carries( frame.Distance ) )
			{
				return Soffit( road, shape.Part, frame );
			}
		}

		return null;
	}

	public static float Soffit( ArchRoadPart road, ArchBridgePart part, ArchFrame frame )
	{
		return frame.Position.z - MathF.Max( 0f, road.Camber ) - Slab( part );
	}

	public static float Slab( ArchBridgePart part ) => MathF.Max( 4f, part.Slab );

	public static float Girder( ArchBridgePart part ) => MathF.Max( 4f, part.Girder );

	public static float Upstand( ArchBridgePart part ) => MathF.Max( 0f, part.Upstand );

	public static float Kerb( ArchBridgePart part ) => Math.Clamp( part.UpstandWidth, 4f, 48f );

	// Both ends of every span: the deck steps into its depth over one cell; a straight bridge survives.
	public static IEnumerable<float> Stations( ArchRoadPart road )
	{
		return new ArchRoadAttachmentService( road ).BridgeStations();
	}

	// Half a post in so the post's face lines up with the fascia - not BarrierInset: a parapet has nowhere to sit back to.
	public static float Stand( ArchRoadPart road, ArchBridgePart part, bool right )
	{
		return MathF.Max( 4f, road.Reach( right ) - MathF.Max( 2f, part.Barrier.PostSize ) * 0.5f );
	}

	// Over a bridge the deck IS the road.
	public static float Edge( ArchRoadPart road, ArchFrame frame, bool right ) => road.Reach( right, frame.WidthScale );

	// Set back from the deck edge, never so far the two sides meet.
	public static float Beam( ArchRoadPart road, ArchBridgePart part, ArchFrame frame, bool right )
	{
		var edge = Edge( road, frame, right );

		return MathF.Max( edge * 0.25f, edge - MathF.Max( 0f, part.EdgeBeam ) );
	}

	// Resolved on the road's OWN frames; the plan comes in so piers clear the roads they fly over.
	public static List<ArchBridgeShape> Shapes(
		ArchRoadPart road,
		ArchPlan plan,
		ArchCurve curve,
		IReadOnlyList<ArchFrame> frames,
		ArchKit kit,
		ArchGround ground )
	{
		var shapes = new List<ArchBridgeShape>();

		foreach ( var part in ArchLayerGate.Enabled( road.Bridges ) )
		{
			var shape = Resolve( road, part, plan, curve, frames, kit, ground );

			if ( shape.IsUsable )
			{
				shapes.Add( shape );
			}
		}

		return shapes;
	}

	public static ArchBridgeShape Resolve(
		ArchRoadPart road,
		ArchBridgePart part,
		ArchPlan plan,
		ArchCurve curve,
		IReadOnlyList<ArchFrame> frames,
		ArchKit kit,
		ArchGround ground )
	{
		return new ArchBridgeShapeBuilder()
			.On( road, part )
			.WithPlan( plan )
			.Along( curve )
			.WithFrames( frames )
			.WithKit( kit )
			.Over( ground )
			.Create();
	}

	internal static ArchBridgeShape ResolveCore(
		ArchRoadPart road,
		ArchBridgePart part,
		ArchPlan plan,
		ArchCurve curve,
		IReadOnlyList<ArchFrame> frames,
		ArchKit kit,
		ArchGround ground )
	{
		var span = Clamped( part, curve.Length );
		var empty = new ArchBridgeShape { Part = part, Road = road, Span = span };

		if ( span.Length < ShortestSpan || frames.Count < 2 )
		{
			return empty;
		}

		span = Lipped( road, part, curve, span, ground, kit );

		var deck = ArchJunction.Clipped( curve, frames, span );

		if ( deck.Count < 2 )
		{
			return empty;
		}

		var grade = Grade( road, frames, kit );
		var under = ArchBridgeGap.Roads( plan );
		var division = Divided( part, span.Length );
		var bents = new List<ArchBridgeBent>();

		foreach ( var offset in division.Inner )
		{
			if ( Stood( road, part, curve, span, offset, division.Step, under, kit, ground, grade, bents ) is { } bent )
			{
				bents.Add( bent );
			}
		}

		var near = Landing( road, part, deck[0], true, ground, grade );
		var far = Landing( road, part, deck[^1], false, ground, grade );
		var wings = new List<ArchWingWall>();

		wings.AddRange( Wings( road, part, near, true, ground ) );
		wings.AddRange( Wings( road, part, far, false, ground ) );

		return new ArchBridgeShape
		{
			Part = part,
			Road = road,
			Span = span,
			Frames = deck,
			Bents = bents,
			Wings = wings,
			Near = near,
			Far = far
		};
	}

	// A span starts at the LIP of the crossing - an abutment inside the bank is buried; a clamp, not a search.
	static ArchRoadSpan Lipped( ArchRoadPart road, ArchBridgePart part, ArchCurve curve, ArchRoadSpan span, ArchGround ground, ArchKit kit )
	{
		var reach = span.Length * Trimmed;
		var lipped = new ArchRoadSpan
		{
			From = span.From + Lip( road, part, curve, span.From, reach, 1f, ground, kit ),
			To = span.To - Lip( road, part, curve, span.To, reach, -1f, ground, kit )
		};

		return lipped.Length < ShortestSpan ? span : lipped;
	}

	// The first clear station is the crest, so the ground an abutment-depth further in must clear too.
	static float Lip( ArchRoadPart road, ArchBridgePart part, ArchCurve curve, float station, float reach, float inward, ArchGround ground, ArchKit kit )
	{
		var clear = MathF.Max( 0f, kit.GroundClearance );
		var depth = MathF.Max( 12f, part.AbutmentDepth );

		for ( var walked = 0f; walked <= reach; walked += ProbeStep )
		{
			if ( Clear( road, part, curve, station + walked * inward, ground, clear )
				&& Clear( road, part, curve, station + (walked + depth) * inward, ground, clear ) )
			{
				return walked;
			}
		}

		return 0f;
	}

	static bool Clear( ArchRoadPart road, ArchBridgePart part, ArchCurve curve, float station, ArchGround ground, float clear )
	{
		if ( !curve.Sample( station, out var frame ) )
		{
			return false;
		}

		var bearing = Soffit( road, part, frame ) - Girder( part ) - clear;

		return ground.Under( frame.Flat, bearing - 1f ) < bearing;
	}

	// Piers stand between the abutments: two bents cut the span into three bays; the ends never get a pier.
	static ArchDivision Divided( ArchBridgePart part, float length )
	{
		return part.Bents > 0
			? ArchDivide.Into( length, part.Bents + 1 )
			: ArchDivide.AtMost( length, MathF.Max( ShortestSpan, part.BentSpacing ) );
	}

	// Hunts outward from the wanted station - the median is the first gap that fits; never stands on a road.
	static ArchBridgeBent? Stood(
		ArchRoadPart road,
		ArchBridgePart part,
		ArchCurve curve,
		ArchRoadSpan span,
		float offset,
		float bay,
		IReadOnlyList<ArchRoadWalk> under,
		ArchKit kit,
		ArchGround ground,
		float grade,
		IReadOnlyList<ArchBridgeBent> placed )
	{
		var depth = MathF.Max( 8f, part.PierDepth );

		foreach ( var station in Hunted( offset, bay * 0.5f ) )
		{
			if ( station < depth * 0.5f || station > span.Length - depth * 0.5f )
			{
				continue;
			}

			if ( !curve.Sample( span.From + station, out var frame ) || Crowded( placed, frame, depth, kit ) )
			{
				continue;
			}

			var bent = Bent( road, part, frame, ground, grade );
			var stands = Stands( road, part, frame, under, kit, ground, bent.Ground );

			if ( stands.Count == 0 )
			{
				continue;
			}

			return bent with { Stands = stands };
		}

		return null;
	}

	static IEnumerable<float> Hunted( float offset, float reach )
	{
		yield return offset;

		for ( var hunted = HuntStep; hunted <= MathF.Max( 0f, reach ); hunted += HuntStep )
		{
			yield return offset - hunted;
			yield return offset + hunted;
		}
	}

	static bool Crowded( IReadOnlyList<ArchBridgeBent> placed, ArchFrame frame, float depth, ArchKit kit )
	{
		foreach ( var bent in placed )
		{
			if ( MathF.Abs( bent.Frame.Distance - frame.Distance ) < depth + MathF.Max( 0f, kit.PierClearance ) )
			{
				return true;
			}
		}

		return false;
	}

	// Where a bent's shafts stand, resolved HERE - the geometry, the ghost and arch_measure cannot disagree.
	static List<ArchBridgeStand> Stands(
		ArchRoadPart road,
		ArchBridgePart part,
		ArchFrame frame,
		IReadOnlyList<ArchRoadWalk> under,
		ArchKit kit,
		ArchGround ground,
		float datum )
	{
		var edges = Edges( road, part, frame );
		var domain = Standing( edges, part );
		var gaps = under.Count == 0
			? new List<ArchGap> { domain }
			: ArchBridgeGap.Across( under, frame, Soffit( road, part, frame ), MathF.Max( 8f, part.PierDepth ), domain, kit );

		var stands = new List<ArchBridgeStand>();

		foreach ( var gap in gaps )
		{
			if ( Stand( part, gap, domain, edges ) is { } stand )
			{
				stands.Add( stand with { Ground = Footed( frame, stand, ground, datum ) } );
			}
		}

		return stands;
	}

	static float Footed( ArchFrame frame, ArchBridgeStand stand, ArchGround ground, float datum )
	{
		var lowest = ground.Under( Flat( frame, stand.From ), datum );

		lowest = MathF.Min( lowest, ground.Under( Flat( frame, stand.To ), datum ) );

		foreach ( var across in stand.Shafts )
		{
			lowest = MathF.Min( lowest, ground.Under( Flat( frame, across ), datum ) );
		}

		return lowest;
	}

	// A blade IS the gap; columns spread across it, held off the edges by half their own width.
	static ArchBridgeStand? Stand( ArchBridgePart part, ArchGap gap, ArchGap domain, ArchGap edges )
	{
		if ( !gap.Holds( Narrowest ) )
		{
			return null;
		}

		var blade = part.Pier == PierStyle.Blade;
		var width = blade ? gap.Width : Math.Clamp( part.PierWidth, 6f, gap.Width );

		// The headstock cantilevers to the deck edge where clear; stops where a gap blocks it.
		return new ArchBridgeStand
		{
			From = gap.From <= domain.From + 1f ? edges.From : gap.From,
			To = gap.To >= domain.To - 1f ? edges.To : gap.To,
			Width = width,
			Shafts = blade ? new List<float> { gap.Middle } : Columns( part, gap, width )
		};
	}

	static List<float> Columns( ArchBridgePart part, ArchGap gap, float width )
	{
		var run = MathF.Max( 0f, gap.Width - width );
		var wanted = Math.Clamp( part.Columns, 1, 12 );
		var columns = Math.Clamp( (int)MathF.Floor( run / MathF.Max( 1f, width * 2f ) ) + 1, 1, wanted );

		if ( columns == 1 )
		{
			return new List<float> { gap.Middle };
		}

		var shafts = new List<float>();

		foreach ( var along in ArchDivide.Into( run, columns - 1 ).Nodes )
		{
			shafts.Add( gap.From + width * 0.5f + along );
		}

		return shafts;
	}

	// Squared to the road's frame so a pier faces the traffic; read by the generator AND the ghost.
	public static List<Vector3> Section( ArchFrame frame, float across, float width, float depth, float height )
	{
		var half = width * 0.5f;
		var reach = frame.Along * (depth * 0.5f);

		var near = frame.Side( across - half ).WithZ( height ) - reach;
		var far = frame.Side( across + half ).WithZ( height ) - reach;

		return new List<Vector3> { near, far, far + reach * 2f, near + reach * 2f };
	}

	public static ArchGap Edges( ArchRoadPart road, ArchBridgePart part, ArchFrame frame )
	{
		return new ArchGap { From = -Beam( road, part, frame, false ), To = Beam( road, part, frame, true ) };
	}

	// Stepped in by the same setback the fascia steps in by; never so far it closes.
	public static ArchGap Standing( ArchGap edges, ArchBridgePart part )
	{
		var pinch = MathF.Min( MathF.Max( 0f, part.EdgeBeam ), MathF.Max( 0f, (edges.Width - 12f) * 0.5f ) );

		return new ArchGap { From = edges.From + pinch, To = edges.To - pinch };
	}

	static ArchBridgeBent Bent( ArchRoadPart road, ArchBridgePart part, ArchFrame frame, ArchGround ground, float grade )
	{
		var bearing = Soffit( road, part, frame ) - Girder( part );

		return new ArchBridgeBent
		{
			Frame = frame,
			Bearing = bearing,
			Ground = ground.Under( frame.Flat, MathF.Min( grade, bearing - NominalRise ) ),
			Stands = new List<ArchBridgeStand>()
		};
	}

	// An abutment reaches the ground fallen away in FRONT of it - probed over its own footprint, so self-limiting.
	static ArchBridgeBent Landing( ArchRoadPart road, ArchBridgePart part, ArchFrame frame, bool near, ArchGround ground, float grade )
	{
		var bearing = Soffit( road, part, frame ) - Girder( part );
		var datum = MathF.Min( grade, bearing - NominalRise );
		var into = near ? frame.Along : -frame.Along;
		var ahead = new Vector2( into.x, into.y );
		var lowest = ground.Under( frame.Flat, datum );

		foreach ( var step in ArchDivide.AtMost( MathF.Max( 12f, part.AbutmentDepth ), ProbeStep ).Nodes )
		{
			lowest = MathF.Min( lowest, ground.Under( frame.Flat + ahead * step, datum ) );
		}

		return new ArchBridgeBent
		{
			Frame = frame,
			Bearing = bearing,
			Ground = lowest,
			Stands = new List<ArchBridgeStand>()
		};
	}

	// No terrain: the road's own approaches, as the LOWER of that and a nominal pier's worth.
	static float Grade( ArchRoadPart road, IReadOnlyList<ArchFrame> frames, ArchKit kit )
	{
		var lowest = float.MaxValue;

		foreach ( var frame in frames )
		{
			lowest = MathF.Min( lowest, frame.Position.z );
		}

		return lowest - MathF.Max( 0f, kit.GroundClearance );
	}

	// Coped to the deck's EDGE, not the soffit - and nothing built where there is no fill to retain.
	static IEnumerable<ArchWingWall> Wings( ArchRoadPart road, ArchBridgePart part, ArchBridgeBent bent, bool near, ArchGround ground )
	{
		if ( !part.Abutments || bent.Rise < Girder( part ) )
		{
			return Array.Empty<ArchWingWall>();
		}

		var frame = bent.Frame;

		return ArchWing.Splayed( frame, near ? -frame.Along : frame.Along,
			Edge( road, frame, false ), Edge( road, frame, true ),
			MathF.Max( 0f, part.WingWall ), part.WingSplay,
			Soffit( road, part, frame ) + Slab( part ), bent.Ground, ground );
	}

	static Vector2 Flat( ArchFrame frame, float across )
	{
		var point = frame.Side( across );

		return new Vector2( point.x, point.y );
	}

	// Clamped and ordered, so a span dragged backwards or off the road's end still describes a stretch.
	public static ArchRoadSpan Clamped( ArchBridgePart part, float length )
	{
		var from = Math.Clamp( MathF.Min( part.From, part.To ), 0f, length );
		var to = Math.Clamp( MathF.Max( part.From, part.To ), 0f, length );

		return new ArchRoadSpan { From = from, To = to };
	}
}