Editor/Barriers/ArchRetaining.cs

Editor-side shape resolver for retaining walls. It computes stations and bays from an ArchBarrierShape and ArchGround, determines which spans retain soil, sizes wall stem and lean, and produces end wing returns for open ends.

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

namespace Sunless.Architecture;

// One station of a retaining run: the level it holds, the grade it stands open to, and which way the fill lies.
// Shared by the two bays either side of it, so a section cannot step across a joint.
public readonly struct ArchRetainingStation
{
	public Vector2 At { get; init; }
	public float Head { get; init; }
	public float Grade { get; init; }
	public Vector2 Fill { get; init; }
	public bool End { get; init; }

	public float Retains => Head - Grade;
}

public readonly struct ArchRetainingBay
{
	public ArchRetainingStation From { get; init; }
	public ArchRetainingStation To { get; init; }
	public int Index { get; init; }

	public float Span => (To.At - From.At).Length;

	public float Retains => MathF.Max( From.Retains, To.Retains );
}

// A freestanding retaining wall: the barrier spline says where it runs, ArchGround says what it holds, and where
// the two faces meet there is no fill and no wall. One resolve, read by the generator, the ghost and the report.
public sealed class ArchRetainingShape
{
	// The thinnest wall worth pouring, and the floor every wing thickness in the tool already takes.
	const float ThinnestStem = 8f;

	public List<ArchRetainingBay> Bays { get; init; } = new();
	public List<ArchWingWall> Returns { get; init; } = new();
	public float Stem { get; init; }
	public float Lean { get; init; }

	public bool IsUsable => Bays.Count > 0;

	public float Retains => Bays.Count == 0 ? 0f : Bays.Max( bay => bay.Retains );

	public static ArchRetainingShape Resolve( ArchBarrierShape barrier, ArchKit kit, ArchGround ground )
	{
		if ( barrier is null || !barrier.IsUsable )
		{
			return new ArchRetainingShape();
		}

		var stations = barrier.Posts.Select( post => Station( post, ground ) ).ToList();
		// A drop shallower than the wall's own footing is a kerb, not something that retains anything.
		var least = ArchGround.Embedment( kit );
		var bays = new List<ArchRetainingBay>();

		foreach ( var bay in barrier.Bays )
		{
			if ( bay.Missing || bay.Index + 1 >= stations.Count )
			{
				continue;
			}

			var standing = new ArchRetainingBay { From = stations[bay.Index], To = stations[bay.Index + 1], Index = bay.Index };

			if ( standing.Retains < least || standing.Span < 1f )
			{
				continue;
			}

			bays.Add( standing );
		}

		var retains = bays.Count == 0 ? 0f : bays.Max( bay => bay.Retains );
		var batter = MathF.Max( 0.02f, kit?.RetainingBatter ?? 0.08f );
		// One section for the whole wall, off its deepest exposure: sized per bay, a joint steps mid-run.
		var stem = MathF.Max( ThinnestStem, retains * batter );

		return new ArchRetainingShape
		{
			Bays = bays,
			Returns = Ends( bays, ground ),
			Stem = stem,
			Lean = Math.Clamp( retains * batter, 0f, stem * 0.5f )
		};
	}

	// The head is the fill's own level, never the line the author drew: a wall dragged along the toe of a bank
	// still comes out as tall as what it holds.
	static ArchRetainingStation Station( ArchBarrierPost post, ArchGround ground )
	{
		var across = ArchBarrierGen.Across( post.Frame );
		var seat = post.Seat;
		var right = ground.Under( post.Flat + across * ArchWing.ProbeStep, seat );
		var left = ground.Under( post.Flat - across * ArchWing.ProbeStep, seat );

		return new ArchRetainingStation
		{
			At = post.Flat,
			Head = MathF.Max( seat, MathF.Max( right, left ) ),
			Grade = MathF.Min( right, left ),
			Fill = right >= left ? across : -across,
			End = post.End
		};
	}

	// Each open end carries on into the bank and dies where it meets its own head - ArchWing's rule, and its
	// refusal: no fill, no wing.
	static List<ArchWingWall> Ends( IReadOnlyList<ArchRetainingBay> bays, ArchGround ground )
	{
		var returns = new List<ArchWingWall>();

		if ( bays.Count == 0 )
		{
			return returns;
		}

		Return( returns, bays[0].From, Heading( bays[0], false ), ground );
		Return( returns, bays[^1].To, Heading( bays[^1], true ), ground );

		return returns;
	}

	static void Return( List<ArchWingWall> into, ArchRetainingStation station, Vector2 heading, ArchGround ground )
	{
		if ( !station.End )
		{
			return;
		}

		var died = ArchWing.Dies( station.At, heading, station.Retains, station.Head, ground, station.Grade );

		if ( died < ArchWing.ProbeStep )
		{
			return;
		}

		var end = station.At + heading * died;

		into.Add( new ArchWingWall
		{
			From = station.At,
			To = end,
			Head = station.Head,
			Ground = MathF.Min( station.Head, ground.Under( end, station.Grade ) ),
			Buried = station.Grade
		} );
	}

	static Vector2 Heading( ArchRetainingBay bay, bool forward )
	{
		var span = bay.To.At - bay.From.At;
		var along = span.Length < 0.05f ? new Vector2( 1f, 0f ) : span.Normal;

		return forward ? along : -along;
	}
}