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 ) );
}
}
}