Editor/Bands/ArchBandGen.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Band coordinate frame — Local for a wall canvas, On(wall) for anything banding in world space.
public readonly struct ArchBandFrame {
	public Vector2 Origin { get; init; }
	public Vector2 Along { get; init; }
	public Vector2 Across { get; init; }

	public static ArchBandFrame Local => new() {
		Origin = Vector2.Zero,
		Along = new Vector2( 1f, 0f ),
		Across = new Vector2( 0f, 1f )
	};

	// Across is the wall's normal, so the pair turns the same way Local's does and the windings hold.
	public static ArchBandFrame On( ArchWall wall ) => new() {
		Origin = wall.Start,
		Along = wall.Direction,
		Across = wall.Normal
	};

	public Vector3 At( float along, float across, float height ) {
		var point = Origin + Along * along + Across * across;

		return new Vector3( point.x, point.y, height );
	}
}

// The one board emitter — back face buried, ends from ArchBandEnd, mitred ends capless.
public static class ArchBandGen {
	// Mitre fold per inch of offset — sign is the corner's own (inside shortens, outside extends).
	public const float SharpestFold = 4f;

	public static float Fold( Vector2 incoming, Vector2 outgoing, float side ) {
		if ( incoming.Length < 0.001f || outgoing.Length < 0.001f ) {
			return 0f;
		}

		var before = Outward( incoming, side );
		var after = Outward( outgoing, side );
		var opened = 1f + Vector2.Dot( before, after );

		if ( opened < 0.05f ) {
			return 0f;
		}

		var fold = Vector2.Dot( before, outgoing.Normal ) / opened;

		// Past the clamp the two boards no longer meet on one plane, so there is no mitre to make: the end
		// caps and butts instead. Clamping the number would have left a suppressed cap over a real hole.
		return MathF.Abs( fold ) > SharpestFold ? 0f : fold;
	}

	public static Vector2 Outward( Vector2 along, float side ) {
		var unit = along.Normal;

		return new Vector2( -unit.y, unit.x ) * side;
	}

	public static void Run(
		ArchMesh canvas,
		ArchBandEnd from,
		ArchBandEnd to,
		ArchBandFrame frame,
		float back,
		float depth,
		float bottom,
		float top,
		ArchBrush brush ) {
		if ( to.Inner - from.Inner < 0.5f || to.Outer - from.Outer < 0.5f || top - bottom < 0.05f ) {
			return;
		}

		var backFrom = frame.At( from.Inner, back, bottom );
		var backTo = frame.At( to.Inner, back, bottom );
		var faceTo = frame.At( to.Outer, back + depth, bottom );
		var faceFrom = frame.At( from.Outer, back + depth, bottom );

		// Wound counter-clockwise, the winding that points every side face outward.
		void Side( Vector3 a, Vector3 b ) {
			canvas.Quad( a, b, b.WithZ( top ), a.WithZ( top ), brush );
		}

		Side( faceTo, faceFrom );

		if ( from.Capped ) {
			Side( faceFrom, backFrom );
		}

		if ( to.Capped ) {
			Side( backTo, faceTo );
		}

		var seat = new List<Vector3> { backFrom, backTo, faceTo, faceFrom };

		canvas.Polygon( seat.Select( point => point.WithZ( top ) ).ToList(), brush );
		canvas.Polygon( seat, brush, true );

		Return( canvas, from, frame, back, depth, bottom, top, brush, true );
		Return( canvas, to, frame, back, depth, bottom, top, brush, false );
	}

	static void Return(
		ArchMesh canvas,
		ArchBandEnd end,
		ArchBandFrame frame,
		float back,
		float depth,
		float bottom,
		float top,
		ArchBrush brush,
		bool starting ) {
		if ( end.Stop != ArchBandStop.Returned || end.Return < 0.5f ) {
			return;
		}

		var seat = frame.At( end.Inner, back, 0f );
		var returned = new ArchBandFrame {
			Origin = new Vector2( seat.x, seat.y ),
			Along = -frame.Across,
			Across = frame.Along * (starting ? -1f : 1f)
		};

		Run(
			canvas,
			new ArchBandEnd( 0f, -depth, ArchBandStop.Mitred ),
			new ArchBandEnd( end.Return, end.Return, ArchBandStop.Cut ),
			returned,
			0f,
			depth,
			bottom,
			top,
			brush );
	}

	// The same board following a climb: a flight's rake, a rising link's floor. Both edges are given
	// their own pair of heights, so a run that starts level and ends raked needs no second call. Its
	// ends carry the same splay a level board's do - a rake reaching a mouth mitres round the jamb
	// like anything else, and a solid prism always caps, so nothing is left open where it does not.
	public static void Raked(
		ArchMesh canvas,
		ArchBandEnd from,
		ArchBandEnd to,
		ArchBandFrame frame,
		float back,
		float depth,
		float lowFrom,
		float lowTo,
		float highFrom,
		float highTo,
		ArchBrush brush ) {
		if ( to.Inner - from.Inner < 0.5f || (highFrom - lowFrom < 0.1f && highTo - lowTo < 0.1f) ) {
			return;
		}

		// An end's two corners share ITS heights, splay or no splay. Carrying the rake on past the
		// mitre tilts the cut face by the slope over the board's depth, and the level board coming
		// the other way round the jamb meets a plane half an inch off its own.
		var lower = new List<Vector3>
		{
			frame.At( from.Inner, back, lowFrom ),
			frame.At( to.Inner, back, lowTo ),
			frame.At( to.Outer, back + depth, lowTo ),
			frame.At( from.Outer, back + depth, lowFrom )
		};

		var upper = new List<Vector3>
		{
			frame.At( from.Inner, back, highFrom ),
			frame.At( to.Inner, back, highTo ),
			frame.At( to.Outer, back + depth, highTo ),
			frame.At( from.Outer, back + depth, highFrom )
		};

		canvas.Prism( lower, upper, brush );
	}
}