Editor/Span/ArchSpanGen.cs

Editor-side generator for arch spans. It computes geometry for beams, haunches, coves and arched voussoirs and emits prisms into an ArchMesh using provided ArchBrush, splitting sections into near/far faces and welding where needed.

File AccessNative Interop
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// One resolved span, whoever asked for it. The ends are the piers' centres and the insets are their half-sections
// along the run, so the solid runs INTO each pier rather than up to its face.
public readonly record struct ArchSpanRun
{
	public Vector2 From { get; init; }
	public Vector2 To { get; init; }
	public float Springing { get; init; }
	public float Top { get; init; }
	public float Thickness { get; init; }
	public float InsetFrom { get; init; }
	public float InsetTo { get; init; }
	public int Segments { get; init; }
	public float Ring { get; init; }
	public PillarSpan Form { get; init; }
}

// The ONE emitter for every span in the tool: the arcade a pillar grid stands and the arch an author picks two
// columns for are the same solid. A second copy of the voussoir sweep is the defect this exists to prevent.
public static class ArchSpanGen
{
	public static void Build( ArchMesh canvas, ArchSpanRun run, ArchBrush brush )
	{
		var direction = run.To - run.From;

		if ( run.Form == PillarSpan.None || direction.Length < 1f )
		{
			return;
		}

		var unit = direction.Normal;
		var start = run.From + unit * run.InsetFrom;
		var end = run.To - unit * run.InsetTo;
		var chord = Vector2.Dot( end - start, unit );
		var side = new Vector2( -unit.y, unit.x ) * MathF.Max( 1f, run.Thickness ) * 0.5f;

		if ( run.Form == PillarSpan.Haunch )
		{
			// A cove needs only room for itself at each end, so it survives a gap a whole arch could not span.
			Haunch( canvas, start, end, unit, side, run, brush );

			return;
		}

		if ( chord < 4f )
		{
			return;
		}

		if ( run.Form == PillarSpan.Beam )
		{
			Beam( canvas, start, end, side, run.Springing, run.Top, brush );

			return;
		}

		Arch( canvas, start, end, unit, side, chord, run, brush );
	}

	static void Beam( ArchMesh canvas, Vector2 start, Vector2 end, Vector2 side, float springing, float top, ArchBrush brush )
	{
		var bottom = new List<Vector3>
		{
			new( start.x + side.x, start.y + side.y, springing ),
			new( end.x + side.x, end.y + side.y, springing ),
			new( end.x - side.x, end.y - side.y, springing ),
			new( start.x - side.x, start.y - side.y, springing )
		};

		var upper = new List<Vector3>();

		foreach ( var point in bottom )
		{
			upper.Add( point.WithZ( top ) );
		}

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

	// The corner where a pier meets what it carries, filled up to a concave quarter-round. Emitted as a fan off
	// that corner rather than as one polygon: a concave section gets no hull, so it would come out of the mesh
	// with no solid to stand on.
	static void Haunch( ArchMesh canvas, Vector2 start, Vector2 end, Vector2 unit, Vector2 side, ArchSpanRun run, ArchBrush brush )
	{
		var rise = MathF.Max( 2f, run.Top - run.Springing );
		var reach = Vector2.Dot( end - start, unit );

		if ( reach < 1f )
		{
			return;
		}

		// Never past halfway, or two coves on a short span grow through each other.
		var cove = MathF.Min( rise, reach * 0.5f );

		Cove( canvas, start, unit, side, run, cove, brush );
		Cove( canvas, end, -unit, side, run, cove, brush );
	}

	static void Cove( ArchMesh canvas, Vector2 face, Vector2 unit, Vector2 side, ArchSpanRun run, float cove, ArchBrush brush )
	{
		var segments = Math.Max( 3, run.Segments );
		var top = run.Top;
		var eye = face + unit * cove;
		var corner = new Vector3( face.x, face.y, top );

		using var welding = canvas.Welding();

		for ( var index = 0; index < segments; index++ )
		{
			var near = Turn( face, eye, unit, cove, index / (float)segments, top );
			var far = Turn( face, eye, unit, cove, (index + 1) / (float)segments, top );

			Sweep( canvas, side, brush, corner, near, far );
		}
	}

	// Round the far corner of the cove's own square: at nothing it stands on the pier face a rise down, at one it
	// stands on the soffit a rise along.
	static Vector3 Turn( Vector2 face, Vector2 eye, Vector2 unit, float cove, float fraction, float top )
	{
		var angle = fraction * MathF.PI * 0.5f;
		var flat = eye - unit * cove * MathF.Cos( angle );

		return new Vector3( flat.x, flat.y, top - cove + cove * MathF.Sin( angle ) );
	}

	// The ring is clamped - unclamped, the extrados flared sideways past its column.
	static void Arch( ArchMesh canvas, Vector2 start, Vector2 end, Vector2 unit, Vector2 side, float chord, ArchSpanRun run, ArchBrush brush )
	{
		var centre = Vector2.Lerp( start, end, 0.5f );
		var segments = Math.Max( 3, run.Segments );

		var band = MathF.Max( 6f, run.Top - run.Springing );
		var ring = Math.Clamp( MathF.Max( 2f, run.Ring ), 2f, band - 2f );
		var rise = band - ring;

		var radius = (chord * chord * 0.25f + rise * rise) / (2f * rise);
		var eye = run.Springing + rise - radius;
		var halfAngle = MathF.Asin( Math.Clamp( chord * 0.5f / radius, -1f, 1f ) );
		var reach = chord * 0.5f;

		Vector3 At( float along, float height ) => new( centre.x + unit.x * along, centre.y + unit.y * along, height );

		Voussoir Cut( float t )
		{
			var angle = -halfAngle + t * halfAngle * 2f;

			return new Voussoir
			{
				Along = radius * MathF.Sin( angle ),
				Inner = eye + radius * MathF.Cos( angle ),
				OuterAlong = Math.Clamp( radius * MathF.Sin( angle ) + MathF.Sin( angle ) * ring, -reach, reach ),
				Outer = eye + radius * MathF.Cos( angle ) + MathF.Cos( angle ) * ring
			};
		}

		for ( var index = 0; index < segments; index++ )
		{
			var near = Cut( index / (float)segments );
			var far = Cut( (index + 1) / (float)segments );

			Sweep( canvas, side, brush,
				At( near.Along, near.Inner ),
				At( far.Along, far.Inner ),
				At( far.OuterAlong, far.Outer ),
				At( near.OuterAlong, near.Outer ) );

			if ( MathF.Min( near.Outer, far.Outer ) >= run.Top - 0.2f || MathF.Abs( far.OuterAlong - near.OuterAlong ) < 0.05f )
			{
				continue;
			}

			Sweep( canvas, side, brush,
				At( near.OuterAlong, MathF.Min( near.Outer, run.Top ) ),
				At( far.OuterAlong, MathF.Min( far.Outer, run.Top ) ),
				At( far.OuterAlong, run.Top ),
				At( near.OuterAlong, run.Top ) );
		}
	}

	readonly struct Voussoir
	{
		public float Along { get; init; }
		public float Inner { get; init; }
		public float OuterAlong { get; init; }
		public float Outer { get; init; }
	}

	static void Sweep( ArchMesh canvas, Vector2 side, ArchBrush brush, params Vector3[] section )
	{
		var near = new List<Vector3>();
		var far = new List<Vector3>();

		foreach ( var point in section )
		{
			near.Add( point + new Vector3( side.x, side.y, 0f ) );
			far.Add( point - new Vector3( side.x, side.y, 0f ) );
		}

		canvas.Prism( near, far, brush );
	}
}