editor/AdvancedMappingTools/Procedural/GatewayGenerator.cs

An editor-side procedural prop generator for a gateway structure. It builds two piers, a flat, round or pointed arch top, optional cap and threshold, and either open, closed doors or metal bars, with each stone emitted as its own part through PropBuilder.

namespace AdvancedMappingTools;

public enum GatewayStyle
{
	[Title( "Open Gate" )] Open,
	[Title( "Closed Doors" )] Doors,
	[Title( "Barred" )] Barred
}

public enum GatewayTop
{
	[Title( "Flat" )] Flat,
	[Title( "Round Arch" )] Round,
	[Title( "Pointed Arch" )] Pointed
}

public sealed class GatewaySettings : PropSettings
{
	[Property, Group( "Portal" ), Title( "Style" ), WideMode]
	public GatewayStyle Style { get; set; } = GatewayStyle.Open;

	[Property, Group( "Portal" ), Title( "Top" ), WideMode, Description( "A flat lintel, or a round arch above the spring line." )]
	public GatewayTop Top { get; set; } = GatewayTop.Flat;

	[Property, HideIf( nameof( Top ), GatewayTop.Flat ), Group( "Portal" ), Title( "Arch Stones" ), Range( 4, 32, slider: false ), WideMode]
	public int ArchSegments { get; set; } = 12;

	[Property, Group( "Portal" ), Range( 40, 512, slider: false ), WideMode]
	public float Width { get; set; } = 96.0f;

	[Property, Group( "Portal" ), Range( 40, 512, slider: false ), WideMode]
	public float Height { get; set; } = 128.0f;

	[Property, Group( "Portal" ), Title( "Pier Width" ), Range( 4, 96, slider: false ), WideMode]
	public float PierWidth { get; set; } = 16.0f;

	[Property, Group( "Portal" ), Title( "Depth" ), Range( 4, 96, slider: false ), WideMode]
	public float Depth { get; set; } = 20.0f;

	[Property, Group( "Portal" ), Title( "Cap" ), WideMode, Description( "A projecting cap over the lintel." )]
	public bool Cap { get; set; } = true;

	[Property, Group( "Portal" ), Title( "Threshold" ), WideMode]
	public bool Threshold { get; set; } = true;

	public override string PropName => Style.ToString();
}

/// <summary>
/// A freestanding gateway: two piers, a lintel with a keystone, an optional cap and
/// threshold, and either open, closed doors or a barred opening. Every stone is a part
/// of its own.
/// </summary>
public sealed class GatewayGenerator : PropGenerator
{
	public override string Id => "gateway";
	public override string Title => "Gateway";
	public override Type SettingsType => typeof( GatewaySettings );

	static void Arch( PropBuilder b, float half, float spring, float depth, float thickness, int segments )
	{
		var inner = half;
		var outer = half + thickness;

		Vector2 At( float radius, float t )
		{
			var angle = MathF.PI * (1.0f - t);

			return new Vector2( radius * MathF.Cos( angle ), spring + radius * MathF.Sin( angle ) );
		}

		for ( var i = 0; i < segments; i++ )
		{
			var t0 = i / (float)segments;
			var t1 = (i + 1) / (float)segments;
			var shape = new List<Vector2> { At( inner, t0 ), At( inner, t1 ), At( outer, t1 ), At( outer, t0 ) };

			b.Side( $"voussoir{i}", "Voussoir", b.Detail, shape, -depth, depth );
		}
	}

	static void Pointed( PropBuilder b, float half, float spring, float depth, float thickness, int segments, float rise )
	{
		// Two circle arcs: upright at the piers, bulging outward, meeting in a point at the top.
		var curve = MeshKit.ArchCurve( -half, half, spring, rise, ArchStyle.Pointed, Math.Max( segments, 4 ) + 1 );

		if ( curve is null || curve.Length < 2 ) return;

		for ( var i = 0; i + 1 < curve.Length; i++ )
		{
			var a = curve[i];
			var c = curve[i + 1];
			var shape = new List<Vector2> { a, c, new( c.x, c.y + thickness ), new( a.x, a.y + thickness ) };

			b.Side( $"pointed{i}", "Arch Stone", b.Detail, shape, -depth, depth );
		}
	}

	protected override void Build( PropSettings settings, PropBuilder b )
	{
		var s = (GatewaySettings)settings;
		var half = MathF.Max( s.Width, 8.0f ) * 0.5f;
		var pier = MathF.Max( s.PierWidth, 2.0f );
		var depth = MathF.Max( s.Depth, 2.0f ) * 0.5f;
		var top = MathF.Max( s.Height, 16.0f );
		var lintel = MathF.Max( pier * 0.6f, 6.0f );

		b.Box( "pier_left", "Left Pier", b.Main, new( -half - pier, -depth, 0.0f ), new( -half, depth, top ) );
		b.Box( "pier_right", "Right Pier", b.Main, new( half, -depth, 0.0f ), new( half + pier, depth, top ) );
		var stones = Math.Clamp( s.ArchSegments, 4, 32 );

		if ( s.Top == GatewayTop.Flat )
		{
			b.Box( "lintel", "Lintel", b.Detail, new( -half - pier, -depth * 1.1f, top ), new( half + pier, depth * 1.1f, top + lintel ) );
			// The keystone hangs a little below the lintel and stays under its top, so no face of
			// the two lies on the other.
			b.Box( "keystone", "Keystone", b.Metal, new( -pier * 0.5f, -depth * 1.15f, top - lintel * 0.25f ), new( pier * 0.5f, depth * 1.15f, top + lintel * 0.9f ) );

			if ( s.Cap )
				b.Box( "cap", "Cap", b.Detail, new( -half - pier * 1.25f, -depth * 1.3f, top + lintel ), new( half + pier * 1.25f, depth * 1.3f, top + lintel + 4.0f ) );
		}
		else
		{
			var rise = s.Top == GatewayTop.Round ? half : half * 1.25f;

			if ( s.Top == GatewayTop.Round ) Arch( b, half, top, depth, lintel, stones );
			else Pointed( b, half, top, depth, lintel, stones, rise );

			if ( s.Cap )
				b.Box( "cap", "Cap", b.Detail, new( -pier * 0.5f, -depth * 1.05f, top + rise + lintel ), new( pier * 0.5f, depth * 1.05f, top + rise + lintel + 4.0f ) );
		}

		if ( s.Threshold )
			b.Box( "threshold", "Threshold", b.Main, new( -half, -depth, 0.0f ), new( half, depth, 1.5f ) );

		if ( s.Style == GatewayStyle.Doors )
		{
			b.Box( "door_left", "Left Door", b.Detail, new( -half, -1.0f, 2.0f ), new( -1.0f, depth * 0.6f, top - 2.0f ) );
			b.Box( "door_right", "Right Door", b.Detail, new( 1.0f, -1.0f, 2.0f ), new( half, depth * 0.6f, top - 2.0f ) );
			b.LooseBox( "handle_left", "Handle", b.Metal, new( -6.0f, depth * 0.6f, top * 0.5f ), new( -4.0f, depth * 0.6f + 2.0f, top * 0.5f + 3.0f ) );
			b.LooseBox( "handle_right", "Handle", b.Metal, new( 4.0f, depth * 0.6f, top * 0.5f ), new( 6.0f, depth * 0.6f + 2.0f, top * 0.5f + 3.0f ) );
		}
		else if ( s.Style == GatewayStyle.Barred )
		{
			var bars = Math.Max( 3, (int)MathF.Round( s.Width / 12.0f ) );

			for ( var i = 1; i < bars; i++ )
			{
				var x = -half + s.Width * i / bars;

				b.Box( $"bar{i}", "Bar", b.Metal, new( x - 1.2f, -1.2f, 2.0f ), new( x + 1.2f, 1.2f, top - 2.0f ) );
			}

			b.Box( "bar_top", "Rail", b.Metal, new( -half, -1.5f, top - 8.0f ), new( half, 1.5f, top - 5.0f ) );
			b.Box( "bar_bottom", "Rail", b.Metal, new( -half, -1.5f, 5.0f ), new( half, 1.5f, 8.0f ) );
		}
	}
}