Editor/Barriers/ArchWing.cs

Editor utility for generating 'arch wing' geometry. It computes wing extents and headings relative to an ArchFrame, probes terrain height to find where a wing "dies" into ground, yields ArchWingWall records, and provides a Build helper that submits geometry to an ArchMesh.

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

namespace Sunless.Architecture;

// Both ends carry their own ground height, so a wing dies into a slope.
public readonly struct ArchWingWall
{
	public Vector2 From { get; init; }
	public Vector2 To { get; init; }
	public float Head { get; init; }
	public float Ground { get; init; }
	public float Buried { get; init; }
}

// Where a wing's splay is measured FROM, the difference between the two structures.
public enum WingSplay
{
	// Off the structure's own direction, angling out: what an abutment wants.
	FromRun,
	// Off the face's own normal, angling back: what a portal wants.
	FromFace
}

// Abutment and portal share one answer: how far before the bank meets their top.
public static class ArchWing
{
	// The one granularity terrain is walked at, so a freestanding wall reads the ground exactly as an abutment does.
	public const float ProbeStep = 24f;

	// A wing retains FILL: where the ground already meets its head, none is built.
	public static IEnumerable<ArchWingWall> Splayed(
		ArchFrame frame,
		Vector3 away,
		float leftEdge,
		float rightEdge,
		float reach,
		float splay,
		float head,
		float datum,
		ArchGround ground,
		WingSplay measured = WingSplay.FromRun )
	{
		if ( reach < 12f )
		{
			yield break;
		}

		var spread = Math.Clamp( splay, 0f, 75f ).DegreeToRadian();

		foreach ( var right in new[] { false, true } )
		{
			var start = Flat( frame, right ? rightEdge : -leftEdge );
			var yaw = Heading( frame, away, right, spread, measured );
			var heading = new Vector2( MathF.Cos( yaw ), MathF.Sin( yaw ) );
			var died = Dies( start, heading, reach, head, ground, datum );

			if ( died < ProbeStep )
			{
				continue;
			}

			var end = start + heading * died;

			yield return new ArchWingWall
			{
				From = start,
				To = end,
				Head = head,
				Ground = MathF.Min( head, ground.Under( end, datum ) ),
				Buried = datum
			};
		}
	}

	// From-face 'back' follows the structure's direction, so it works at either end of a bore.
	static float Heading( ArchFrame frame, Vector3 away, bool right, float spread, WingSplay measured )
	{
		var along = MathF.Atan2( away.y, away.x );

		if ( measured == WingSplay.FromRun )
		{
			return along + (right ? spread : -spread);
		}

		var outward = right ? frame.Across : -frame.Across;
		var face = MathF.Atan2( outward.y, outward.x );
		var turn = MathF.Atan2( MathF.Sin( along - face ), MathF.Cos( along - face ) );

		return face + MathF.Sign( turn ) * spread;
	}

	// Authored length is a MAXIMUM: past the bank's meeting point, a wing lies flat.
	public static float Dies( Vector2 start, Vector2 heading, float reach, float head, ArchGround ground, float datum )
	{
		for ( var walked = ProbeStep; walked <= reach; walked += ProbeStep )
		{
			if ( ground.Under( start + heading * walked, datum ) >= head )
			{
				return walked;
			}
		}

		return reach;
	}

	// Dies just proud of the ground: the end is meant to disappear into the bank.
	public static void Build( ArchMesh canvas, IEnumerable<ArchWingWall> wings, float thickness, ArchKit kit, ArchBrush brush )
	{
		var embedment = ArchGround.Embedment( kit );
		var half = MathF.Max( 2f, thickness ) * 0.5f;

		foreach ( var wing in wings )
		{
			var foot = wing.Buried - embedment;
			var died = Math.Clamp( wing.Ground + ArchContact.Proud( kit ), foot + 1f, wing.Head );

			canvas.Rake( wing.From, wing.To, -half, half, foot, wing.Ground - embedment, wing.Head, died, brush );
		}
	}

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

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