editor/AdvancedMappingTools/Procedural/ArchGenerator.cs

Editor generator for procedural arches. ArchSettings holds the shape (round, pointed, horseshoe, ogee, custom profile, and so on), rise, ring section, keystone, piers, bases, imposts and wall options; ArchGenerator turns that into a PartPlan of separate parts (piers, ring or individual voussoirs, keystone, spandrel walls) using MeshKit sweeps along the opening curve. CurvedArchGenerator is the same arch row built along a PathCurve or spiral.

namespace AdvancedMappingTools;
/// <summary>
/// The first entries match <see cref="ArchStyle"/> number for number.
/// </summary>
public enum ArchShape
{
	Flat,
	[Title( "Round / Roman" )] Round,
	[Title( "Pointed / Gothic" )] Pointed,
	Segmental,
	Tudor,
	Lancet,
	Horseshoe,
	Elliptical,
	Ogee,
	[Title( "Multi Centered" )] ThreeCentered,
	[Title( "Custom Profile" )] Custom,
	[Title( "Full Circle" )] Circular
}

public enum ArchOuterShape
{
	[Title( "Same As Opening" )] Same,
	[Title( "Flat Top" )] Flat,
	Round,
	Pointed
}

public enum ArchRingSection
{
	Rectangular,
	Chamfered,
	Rounded,
	Stepped,
	Custom
}

public enum PierShape
{
	Square,
	Round,
	Octagonal
}

public class ArchSettings : GeneratorSettings
{
	/// <summary>
	/// The clear length between the outer piers, set by the tool.
	/// </summary>
	public float Length { get; set; } = 128.0f;

	[Property, Group( "Arch" ), WideMode]
	public ArchShape Shape { get; set; } = ArchShape.Round;

	[Property, Group( "Arch" ), Range( 0, 4096, slider: false ), WideMode, Description( "Height of the arch over its spring line. Zero uses the natural height of the shape." )]
	public float Rise { get; set; } = 0.0f;

	[Property, Group( "Arch" ), Title( "Spring Height" ), Range( 0, 4096, slider: false ), WideMode, Description( "Where the arch starts above the ground. A full circle starts here at its lowest point." )]
	public float SpringHeight { get; set; } = 96.0f;

	[Property, Group( "Arch" ), Range( 1, 512, slider: false ), WideMode, Description( "Of the ring, measured across it." )]
	public float Thickness { get; set; } = 16.0f;

	[Property, Group( "Arch" ), Range( 1, 1024, slider: false ), WideMode]
	public float Depth { get; set; } = 24.0f;

	[Property, Group( "Arch" ), Range( 4, 64, slider: false ), WideMode]
	public int Segments { get; set; } = 16;

	[Property, Group( "Arch" ), Title( "Connected Arches" ), Range( 1, 64, slider: false ), WideMode, Description( "Arches in a row, sharing the piers between them." )]
	public int Bays { get; set; } = 1;

	[Property, Group( "Ring" ), Title( "Outer Shape" ), WideMode, Description( "The ring follows the opening, or has a shape of its own on the outside." )]
	public ArchOuterShape OuterShape { get; set; } = ArchOuterShape.Same;

	[Property, Group( "Ring" ), Title( "Cross Section" ), WideMode]
	public ArchRingSection Section { get; set; } = ArchRingSection.Rectangular;

	[Property, HideIf( nameof( Section ), ArchRingSection.Rectangular ), Group( "Ring" ), Title( "Edge Size" ), Range( 0, 128, slider: false ), WideMode, Description( "Of a chamfered, rounded or stepped cross section." )]
	public float EdgeSize { get; set; } = 3.0f;

	[Property, ShowIf( nameof( Section ), ArchRingSection.Rounded ), Group( "Ring" ), Title( "Profile Subdivisions" ), Range( 1, 16, slider: false ), WideMode, Description( "Steps around a rounded edge." )]
	public int ProfileSubdivisions { get; set; } = 3;

	[Property, Group( "Ring" ), Title( "Smooth Surface" ), WideMode, Description( "Curved faces shade smoothly. Off shows every segment." )]
	public bool SmoothSurface { get; set; } = true;

	[Property, Group( "Ring" ), Title( "Separate Stones" ), WideMode, Description( "Builds the ring from single stones, each one its own part." )]
	public bool Voussoirs { get; set; } = false;

	[Property, ShowIf( nameof( Voussoirs ), true ), Group( "Ring" ), Title( "Stone Count" ), Range( 3, 63, slider: false ), WideMode]
	public int Stones { get; set; } = 13;

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

	[Property, ShowIf( nameof( Keystone ), true ), Group( "Keystone" ), Title( "Keystone Width" ), Range( 1, 512, slider: false ), WideMode]
	public float KeystoneWidth { get; set; } = 20.0f;

	[Property, ShowIf( nameof( Keystone ), true ), Group( "Keystone" ), Title( "Above The Ring" ), Range( 0, 256, slider: false ), WideMode]
	public float KeystoneRise { get; set; } = 6.0f;

	[Property, ShowIf( nameof( Keystone ), true ), Group( "Keystone" ), Title( "Below The Ring" ), Range( 0, 256, slider: false ), WideMode]
	public float KeystoneDrop { get; set; } = 3.0f;

	[Property, ShowIf( nameof( Keystone ), true ), Group( "Keystone" ), Title( "Standing Proud" ), Range( 0, 64, slider: false ), WideMode, Description( "How far it stands out from both faces of the ring." )]
	public float KeystoneProud { get; set; } = 2.0f;

	[Property, Group( "Supports" ), Title( "Piers" ), WideMode, Description( "A full circle stands by itself and has none." )]
	public bool Piers { get; set; } = true;

	[Property, Group( "Supports" ), Title( "Pier Shape" ), WideMode]
	public PierShape PierShape { get; set; } = PierShape.Square;

	[Property, Group( "Supports" ), Title( "Pier Width" ), Range( 1, 512, slider: false ), WideMode, Description( "Also the distance between connected arches." )]
	public float PierWidth { get; set; } = 24.0f;

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

	[Property, ShowIf( nameof( Bases ), true ), Group( "Supports" ), Title( "Base Height" ), Range( 1, 256, slider: false ), WideMode]
	public float BaseHeight { get; set; } = 12.0f;

	[Property, ShowIf( nameof( Bases ), true ), Group( "Supports" ), Title( "Base Overhang" ), Range( 0, 64, slider: false ), WideMode]
	public float BaseOverhang { get; set; } = 3.0f;

	[Property, Group( "Supports" ), Title( "Imposts" ), WideMode, Description( "A block on the pier where the arch starts." )]
	public bool Imposts { get; set; } = true;

	[Property, ShowIf( nameof( Imposts ), true ), Group( "Supports" ), Title( "Impost Height" ), Range( 1, 256, slider: false ), WideMode]
	public float ImpostHeight { get; set; } = 8.0f;

	[Property, ShowIf( nameof( Imposts ), true ), Group( "Supports" ), Title( "Impost Overhang" ), Range( 0, 64, slider: false ), WideMode]
	public float ImpostOverhang { get; set; } = 2.0f;

	[Property, Group( "Wall" ), Title( "Wall Above" ), WideMode, Description( "Fills the corners next to the arch up to a straight top." )]
	public bool WallAbove { get; set; } = true;

	[Property, ShowIf( nameof( WallAbove ), true ), Group( "Wall" ), Title( "Wall Top" ), Range( 8, 8192, slider: false ), WideMode, Description( "Height of the top above the ground. It stays above the arch." )]
	public float WallTop { get; set; } = 192.0f;

	[Property, ShowIf( nameof( WallAbove ), true ), Group( "Wall" ), Title( "Wall Depth" ), Range( 1, 1024, slider: false ), WideMode, Description( "A wall thinner than the ring lets the ring stand out." )]
	public float WallDepth { get; set; } = 20.0f;

	[Property, ShowIf( nameof( Shape ), ArchShape.Custom ), Group( "Custom" ), Title( "Profile Points" ), WideMode, Description( "With the Custom Profile shape: the left half as across,up pairs from 0,0 at the spring point to 1,1 at the crown, split by semicolons." )]
	public string CustomProfile { get; set; } = "0,0; 0.04,0.5; 0.3,0.82; 1,1";

	[Property, ShowIf( nameof( Section ), ArchRingSection.Custom ), Group( "Custom" ), Title( "Section Points" ), WideMode, Description( "With the Custom cross section: across,up pairs from 0 to 1, split by semicolons. Across runs through the depth, up from the opening outward." )]
	public string CustomSection { get; set; } = "0.15,0; 0.85,0; 0.85,0.35; 1,0.35; 1,1; 0,1; 0,0.35; 0.15,0.35";

	[Property, Group( "Materials" ), Title( "Ring" ), ResourceType( "vmat" ), WideMode]
	public string RingMaterial { get; set; }

	[Property, Group( "Materials" ), Title( "Every Second Stone" ), ResourceType( "vmat" ), WideMode, Description( "A second material for striped arches. Empty uses the ring material." )]
	public string AlternateMaterial { get; set; }

	[Property, ShowIf( nameof( Keystone ), true ), Group( "Materials" ), Title( "Keystone" ), ResourceType( "vmat" ), WideMode]
	public string KeystoneMaterial { get; set; }

	[Property, Group( "Materials" ), Title( "Piers" ), ResourceType( "vmat" ), WideMode]
	public string PierMaterial { get; set; }

	[Property, Group( "Materials" ), Title( "Bases And Imposts" ), ResourceType( "vmat" ), WideMode]
	public string TrimMaterial { get; set; }

	[Property, ShowIf( nameof( WallAbove ), true ), Group( "Materials" ), Title( "Wall" ), ResourceType( "vmat" ), WideMode]
	public string WallMaterial { get; set; }
}

public sealed class CurvedArchSettings : ArchSettings
{
	/// <summary>
	/// The line the arches stand on, set by the tool.
	/// </summary>
	public PathCurve Path { get; set; } = PathCurve.FromPoints( [Vector3.Zero, new Vector3( 256.0f, 0.0f, 0.0f )] );

	[Property, Group( "Path" ), WideMode]
	public PathLayout Layout { get; set; } = PathLayout.Drawn;

	[Property, Group( "Path" ), Title( "Spiral Turns" ), Range( 0.1f, 32, slider: false ), WideMode]
	public float Turns { get; set; } = 0.5f;

	[Property, Group( "Path" ), Title( "Spiral Clockwise" ), WideMode]
	public bool Clockwise { get; set; } = false;

	[Property, Group( "Path" ), Title( "Climb" ), Range( -4096, 4096, slider: false ), WideMode, Description( "Height added along the path, on top of the height of its points." )]
	public float Climb { get; set; } = 0.0f;

	[Property, Group( "Path" ), Title( "Path Detail" ), Range( 1, 256, slider: false ), WideMode, Description( "Longest straight piece along the path." )]
	public float PathDetail { get; set; } = 8.0f;
}

/// <summary>
/// An arch of independent parts: piers with base and impost, the ring, its keystone or single
/// stones, and the wall in the corners next to it. Ring and keystone are one cross section
/// carried along the curve of the opening, so each has a closed surface all the way round and
/// a neighbour only ever touches it. Nothing overlaps, and any part can be replaced by hand.
/// </summary>
public class ArchGenerator : IPartGenerator
{
	const string RingCategory = "ring";
	const string KeystoneCategory = "keystone";
	const string PierCategory = "pier";
	const string TrimCategory = "trim";
	const string WallCategory = "wall";

	const int RoundSides = 16;
	const float SmallestThickness = 0.5f;
	const float WallClearance = 1.0f;
	const float StoneDepthRange = 0.1f;
	const float FlatArchSplay = 0.5f;
	const float PointedOuterRise = 1.2f;
	const float FanSpacing = 1.0f;
	const float OutwardStep = 0.7f;

	public virtual string Id => "arch";
	public virtual string Title => "Arch";
	public virtual Type SettingsType => typeof( ArchSettings );
	public IReadOnlyList<string> SeedNames => [RingCategory, KeystoneCategory, PierCategory, TrimCategory, WallCategory];

	/// <summary>
	/// The line the arches stand on, or nothing for a straight row along X.
	/// </summary>
	protected virtual PathTrack Route( ArchSettings settings ) => null;

	/// <summary>
	/// The curve of one ring, at any distance from the opening.
	/// </summary>
	sealed class RingCurve( Vector2[] inner, Vector2[] outer, float thickness, float spring, bool turned = false )
	{
		readonly Dictionary<float, Vector2[]> _curves = [];

		public int Last => inner.Length - 1;
		public int Crown => inner.Length / 2;

		/// <summary>
		/// This curve turned over at the spring line. It runs the other way, so outside stays outside.
		/// </summary>
		public RingCurve TurnedOver => new( inner, outer, thickness, spring, true );

		public Vector2[] At( float offset )
		{
			if ( _curves.TryGetValue( offset, out var known ) ) return known;

			var curve = offset == 0.0f ? [.. inner]
				: outer is null ? MeshKit.OffsetCurve( inner, offset )
				: [.. inner.Zip( outer, ( a, b ) => a + (b - a) * (offset / thickness) )];

			if ( offset > 0.0f ) Rest( curve );

			if ( turned )
			{
				Array.Reverse( curve );

				for ( var i = 0; i < curve.Length; i++ )
					curve[i] = curve[i].WithY( spring * 2.0f - curve[i].y );
			}

			return _curves[offset] = curve;
		}

		/// <summary>
		/// A ring stands on the spring line, even where the curve leaves it at an angle.
		/// </summary>
		void Rest( Vector2[] curve )
		{
			Clip( curve );
			Array.Reverse( curve );
			Clip( curve );
			Array.Reverse( curve );
		}

		void Clip( Vector2[] curve )
		{
			if ( curve[0].y >= spring )
			{
				var step = curve[1] - curve[0];

				// Level ends belong to a flat arch, which keeps its height there.
				if ( step.y > 0.001f )
				{
					curve[0] -= step * ((curve[0].y - spring) / step.y);
					curve[0] = curve[0].WithY( spring );
				}

				return;
			}

			var first = Array.FindIndex( curve, x => x.y >= spring );

			if ( first <= 0 ) return;

			var from = curve[first - 1];
			var to = curve[first];
			var point = from + (to - from) * ((spring - from.y) / (to.y - from.y));

			for ( var i = 0; i < first; i++ )
				curve[i] = point;
		}
	}

	/// <summary>
	/// Everything the parts of one arch share.
	/// </summary>
	sealed class Build( ArchSettings settings, GenContext context, PathTrack track, float thickness )
	{
		public ArchSettings S => settings;
		public GenContext Context => context;
		public float Thickness => thickness;
		public float Top { get; set; }

		public Vector3 Place( float x, float y, float z )
		{
			if ( track is null )
				return new Vector3( x, y, z );
			// Rings without piers and horseshoe bulges may extend past the path.
			// Clamping them onto its endpoint collapses faces into vertical lines.
			if ( !track.Closed && x < 0 )
				return track.PlaceAt( 0, y, z ) + track.Frames[0].Forward * x;
			if ( !track.Closed && x > track.Length )
				return track.PlaceAt( track.Length, y, z ) + track.Frames[^1].Forward * (x - track.Length);
			return track.PlaceAt( x, y, z );
		}
		public bool Straight => track is null;

		/// <summary>
		/// A box, or the piece of one that follows the path.
		/// </summary>
		public void Block( MeshKit kit, float x0, float x1, float y0, float y1, float z0, float z1 )
		{
			kit.Hex( Place( x0, y0, z0 ), Place( x1, y0, z0 ), Place( x1, y1, z0 ), Place( x0, y1, z0 ),
				Place( x0, y0, z1 ), Place( x1, y0, z1 ), Place( x1, y1, z1 ), Place( x0, y1, z1 ) );
		}

		/// <summary>
		/// Carry a section along a curve between two stations. A section is across, out from the opening.
		/// </summary>
		public void Carry( MeshKit kit, RingCurve curve, int from, int to, IReadOnlyList<Vector2> section )
		{
			var rings = section.Select( p =>
			{
				var line = curve.At( p.y );
				return Enumerable.Range( from, to - from + 1 ).Select( i => Place( line[i].x, p.x, line[i].y ) ).ToArray();
			} ).ToArray();

			kit.Sweep( rings, false );
		}
	}

	public PartPlan Generate( GeneratorSettings settings, GenContext context )
	{
		var s = (ArchSettings)settings;
		var root = context.Group( "root", Title, "Building", RingCategory );
		var track = Route( s );

		var circle = s.Shape == ArchShape.Circular;
		var piers = s.Piers && !circle;
		var gap = MathF.Max( s.PierWidth, 1.0f );
		var bays = Math.Max( s.Bays, 1 );
		var origin = track is not null && piers ? gap : 0.0f;
		var clear = track is null ? s.Length : track.Length - (piers ? gap * 2.0f : 0.0f);
		var span = (clear - gap * (bays - 1)) / bays;

		if ( span < 4.0f ) return root;

		// An arch that bulges past its spring points needs room to its neighbour.
		var bulge = MathF.Max( -Opening( s, s.Shape, 0.0f, span, s.SpringHeight, RiseOf( s, span ) ).Min( p => p.x ), 0.0f );

		if ( bays > 1 && gap < (bulge + SmallestThickness) * 2.0f )
		{
			gap = (bulge + SmallestThickness) * 2.0f;
			span = (clear - gap * (bays - 1)) / bays;

			if ( span < 4.0f ) return root;
		}

		var rise = RiseOf( s, span );
		var spring = circle ? s.SpringHeight + span * 0.5f : s.SpringHeight;

		var thickness = RingThickness( s, span, rise, spring, circle ? float.MaxValue : bays > 1 ? gap * 0.5f : piers ? gap : float.MaxValue );
		var build = new Build( s, context, track, thickness );
		var section = Section( s, thickness );
		var rim = section.Max( x => x.y );
		var reach = rim + (s.Keystone ? s.KeystoneRise : 0.0f);

		build.Top = MathF.Max( s.WallTop, Curve( s, 0.0f, span, spring, rise, thickness, circle ).At( reach ).Max( x => x.y ) + WallClearance );

		if ( piers )
		{
			for ( var i = 0; i <= bays; i++ )
			{
				var right = origin + i * (span + gap);
				var id = $"pier{i:00}";

				Pier( root.Add( context.Group( id, $"Pier {i + 1}", "Pier", PierCategory ) ), build, id, right - gap, right, spring );
			}
		}

		for ( var b = 0; b < bays; b++ )
		{
			var id = $"bay{b:00}";
			var bay = root.Add( context.Group( id, $"Arch {b + 1}", "Bay", RingCategory ) );
			var x0 = origin + b * (span + gap);
			var x1 = x0 + span;
			var curve = Curve( s, x0, x1, spring, rise, thickness, circle );
			var outline = curve.At( rim );

			// A bay ends on its pier or halfway between two. An arch that stands alone ends with its ring.
			var left = b > 0 ? x0 - gap * 0.5f : piers || circle ? x0 - gap : outline.Min( p => p.x );
			var right = b < bays - 1 ? x1 + gap * 0.5f : piers || circle ? x1 + gap : outline.Max( p => p.x );

			Ring( bay, build, id, curve, section, rim, left, right, spring );
		}

		return root;
	}

	static float RiseOf( ArchSettings s, float span )
	{
		if ( s.Shape == ArchShape.Flat ) return 0.0f;
		if ( s.Shape == ArchShape.Circular ) return span * 0.5f;
		if ( s.Rise > 0.0f ) return s.Rise;
		if ( s.Shape == ArchShape.Custom ) return span * 0.5f;

		return MeshKit.ArchRise( (ArchStyle)(int)s.Shape, span );
	}

	/// <summary>
	/// The thickness a ring may have: where its foot leaves the opening has to stay on its share of the pier.
	/// </summary>
	static float RingThickness( ArchSettings s, float span, float rise, float spring, float reach )
	{
		var thickness = MathF.Max( s.Thickness, SmallestThickness );
		var curve = Curve( s, 0.0f, span, spring, rise, thickness, s.Shape == ArchShape.Circular );
		var bulge = -curve.At( 0.0f ).Min( p => p.x );
		var foot = -curve.At( thickness ).Min( p => p.x );

		// A bulge stays clear of the end of the bay. A foot may stand right on it.
		var room = bulge > 0.0f ? reach - WallClearance : reach;

		return foot > room ? MathF.Max( thickness * (room - bulge) / (foot - bulge), SmallestThickness ) : thickness;
	}

	static RingCurve Curve( ArchSettings s, float x0, float x1, float spring, float rise, float thickness, bool circle )
	{
		// A full circle keeps its outside inside the bay, the other half of it hangs from here.
		var inset = circle ? thickness : 0.0f;
		var inner = Opening( s, s.Shape, x0 + inset, x1 - inset, spring, circle ? rise - inset : rise );
		var segments = inner.Length - 1;
		var outer = s.OuterShape switch
		{
			ArchOuterShape.Flat => Boxed( inner.Length, x0 - thickness, x1 + thickness, spring, spring + rise + thickness ),
			ArchOuterShape.Round => Opening( s, ArchShape.Round, x0 - thickness, x1 + thickness, spring, rise + thickness, segments ),
			ArchOuterShape.Pointed => Opening( s, ArchShape.Pointed, x0 - thickness, x1 + thickness, spring, rise * PointedOuterRise + thickness, segments ),
			_ => s.Shape == ArchShape.Flat ? Splayed( inner, thickness, spring ) : null
		};

		return new RingCurve( inner, circle ? null : outer, thickness, spring );
	}

	/// <summary>
	/// The top of a flat arch: its stones spread upward, so it wedges itself in between its piers.
	/// </summary>
	static Vector2[] Splayed( Vector2[] inner, float thickness, float spring )
	{
		var first = inner[0].x;
		var last = inner[^1].x;
		var spread = thickness * FlatArchSplay;

		return [.. inner.Select( p => new Vector2( first - spread + (last - first + spread * 2.0f) * (p.x - first) / (last - first), spring + thickness ) )];
	}

	/// <summary>
	/// Points along three sides of a box around an opening, as many as the opening has.
	/// </summary>
	static Vector2[] Boxed( int count, float x0, float x1, float spring, float top )
	{
		var height = top - spring;
		var width = x1 - x0;
		var total = height * 2.0f + width;
		var last = count - 1;
		var result = new Vector2[count];

		for ( var i = 0; i < count; i++ )
		{
			var along = total * i / last;

			result[i] = along < height ? new Vector2( x0, spring + along )
				: along < height + width ? new Vector2( x0 + along - height, top )
				: new Vector2( x1, top - (along - height - width) );
		}

		// The box needs its corners, whatever the spacing of the points.
		var corner = Math.Clamp( (int)MathF.Round( height / total * last ), 1, last / 2 );

		result[corner] = new Vector2( x0, top );
		result[last - corner] = new Vector2( x1, top );

		return result;
	}

	static Vector2[] Opening( ArchSettings s, ArchShape shape, float x0, float x1, float spring, float rise, int segments = -1 )
	{
		var count = segments > 0 ? segments : s.Segments;

		switch ( shape )
		{
			case ArchShape.Flat:
			{
				var even = Math.Max( count, 2 ) / 2 * 2;

				return [.. Enumerable.Range( 0, even + 1 ).Select( i => new Vector2( x0 + (x1 - x0) * i / even, spring ) )];
			}

			case ArchShape.Custom:
				return MeshKit.ArchCurve( x0, x1, spring, rise, s.CustomProfile, count ) ?? MeshKit.ArchCurve( x0, x1, spring, rise, ArchStyle.Round, count );

			case ArchShape.Circular:
				return MeshKit.ArchCurve( x0, x1, spring, rise, ArchStyle.Round, count );

			default:
				return MeshKit.ArchCurve( x0, x1, spring, rise, (ArchStyle)(int)shape, count );
		}
	}

	/// <summary>
	/// The cross section of the ring as across, out from the opening, counter clockwise.
	/// </summary>
	static List<Vector2> Section( ArchSettings s, float thickness )
	{
		var h = s.Depth * 0.5f;
		var t = thickness;
		var e = Math.Clamp( s.EdgeSize, 0.0f, MathF.Min( h, t * 0.5f ) - 0.01f );

		switch ( s.Section )
		{
			case ArchRingSection.Chamfered when e > 0.0f:
				return [new( -h + e, 0 ), new( h - e, 0 ), new( h, e ), new( h, t - e ), new( h - e, t ), new( -h + e, t ), new( -h, t - e ), new( -h, e )];

			case ArchRingSection.Rounded when e > 0.0f:
			{
				var steps = Math.Max( s.ProfileSubdivisions, 1 );
				var result = new List<Vector2>();

				void Corner( float cy, float cr, float start )
				{
					for ( var i = 0; i <= steps; i++ )
					{
						var angle = (start + 90.0f * i / steps).DegreeToRadian();
						result.Add( new Vector2( cy + MathF.Cos( angle ) * e, cr + MathF.Sin( angle ) * e ) );
					}
				}

				Corner( h - e, e, 270.0f );
				Corner( h - e, t - e, 0.0f );
				Corner( -h + e, t - e, 90.0f );
				Corner( -h + e, e, 180.0f );

				return result;
			}

			case ArchRingSection.Stepped when e > 0.0f:
				return [new( -h + e, 0 ), new( h - e, 0 ), new( h - e, t * 0.5f ), new( h, t * 0.5f ), new( h, t ), new( -h, t ), new( -h, t * 0.5f ), new( -h + e, t * 0.5f )];

			case ArchRingSection.Custom:
			{
				var points = MeshKit.ParsePoints( s.CustomSection );

				if ( points.Count < 3 ) break;

				var shape = points.Select( p => new Vector2( (p.x - 0.5f) * s.Depth, p.y * t ) ).ToList();
				var area = 0.0f;

				for ( var i = 0; i < shape.Count; i++ )
					area += shape[i].x * shape[(i + 1) % shape.Count].y - shape[(i + 1) % shape.Count].x * shape[i].y;

				if ( area < 0.0f ) shape.Reverse();

				return shape;
			}
		}

		return [new( -h, 0 ), new( h, 0 ), new( h, t ), new( -h, t )];
	}

	static void Pier( PartPlan pier, Build build, string id, float x0, float x1, float spring )
	{
		var s = build.S;
		var context = build.Context;
		var trim = context.Material( s.TrimMaterial );
		var stone = context.Material( s.PierMaterial );
		var depth = s.Depth * 0.5f;
		var middle = (x0 + x1) * 0.5f;
		var radius = MathF.Min( x1 - x0, s.Depth ) * 0.5f;
		var bottom = s.Bases ? MathF.Min( s.BaseHeight, spring * 0.3f ) : 0.0f;
		var top = spring - (s.Imposts ? MathF.Min( s.ImpostHeight, spring * 0.3f ) : 0.0f);

		if ( top <= bottom ) return;

		void Upright( MeshKit kit, float grow, float z0, float z1 )
		{
			if ( s.PierShape == PierShape.Square )
			{
				build.Block( kit, x0 - grow, x1 + grow, -depth - grow, depth + grow, z0, z1 );
				return;
			}

			var center = build.Place( middle, 0.0f, 0.0f );

			kit.Cylinder( center, radius + grow, radius + grow, z0, z1, s.PierShape == PierShape.Round ? RoundSides : 8 );
		}

		if ( s.Bases && bottom > 0.0f )
			pier.Add( context.Solid( $"{id}/base", "Base", "PierBase", TrimCategory, trim, kit => Upright( kit, s.BaseOverhang, 0.0f, bottom ) ) );

		pier.Add( context.Solid( $"{id}/shaft", "Shaft", "PierShaft", PierCategory, stone, kit => Upright( kit, 0.0f, bottom, top ) ) );

		if ( s.Imposts && top < spring )
		{
			pier.Add( context.Solid( $"{id}/impost", "Impost", "Impost", TrimCategory, trim,
				kit => build.Block( kit, x0 - s.ImpostOverhang, x1 + s.ImpostOverhang, -depth - s.ImpostOverhang, depth + s.ImpostOverhang, top, spring ) ) );
		}
	}

	static void Ring( PartPlan bay, Build build, string id, RingCurve curve, List<Vector2> section, float rim, float left, float right, float spring )
	{
		var s = build.S;
		var context = build.Context;
		var last = curve.Last;
		var crown = curve.Crown;
		var opening = curve.At( 0.0f );
		var keystone = s.Keystone && crown >= 2;

		var length = 0.0f;
		for ( var i = 0; i < last; i++ ) length += opening[i].Distance( opening[i + 1] );

		var keyHalf = keystone ? Math.Clamp( (int)MathF.Round( s.KeystoneWidth * 0.5f / (length / last) ), 1, crown - 1 ) : 0;
		var from = crown - keyHalf;
		var to = crown + keyHalf;
		var stones = s.Voussoirs ? Math.Clamp( (s.Stones - (keystone ? 1 : 0)) / 2, 1, from ) : 1;

		var ring = context.Material( s.RingMaterial );
		var alternate = string.IsNullOrEmpty( s.AlternateMaterial ) ? ring : context.Material( s.AlternateMaterial );

		void Stretch( string name, string label, string kind, int a, int b, Material material, int index )
		{
			var partId = $"{id}/{name}";

			if ( !s.Voussoirs )
			{
				bay.Add( context.Solid( partId, label, kind, RingCategory, material, kit =>
				{
					kit.SmoothingAngle = s.SmoothSurface ? MeshKit.DefaultSmoothing : 0.0f;
					build.Carry( kit, curve, a, b, section );
				} ) );

				return;
			}

			var depth = 1.0f - s.Variation * StoneDepthRange * context.Stream( RingCategory, partId ).Range( 0.0f, 1.0f );
			var shaped = section.Select( p => p.WithX( p.x * depth ) ).ToList();

			bay.Add( context.Solid( partId, label, kind, RingCategory, index % 2 == 0 ? ring : alternate, kit =>
			{
				kit.SmoothingAngle = s.SmoothSurface ? MeshKit.DefaultSmoothing : 0.0f;
				build.Carry( kit, curve, a, b, shaped );
			} ) );
		}

		if ( !keystone && !s.Voussoirs )
		{
			Stretch( "ring", "Ring", "ArchRing", 0, last, ring, 0 );
		}
		else
		{
			for ( var i = 0; i < stones; i++ )
			{
				var a = from * i / stones;
				var b = from * (i + 1) / stones;

				Stretch( s.Voussoirs ? $"stone_left{i:00}" : "ring_left", s.Voussoirs ? $"Left Stone {i + 1}" : "Left Ring", s.Voussoirs ? "Voussoir" : "ArchRing", a, b, ring, i );
				Stretch( s.Voussoirs ? $"stone_right{i:00}" : "ring_right", s.Voussoirs ? $"Right Stone {i + 1}" : "Right Ring", s.Voussoirs ? "Voussoir" : "ArchRing", last - b, last - a, ring, i );
			}
		}

		var above = rim + (keystone ? s.KeystoneRise : 0.0f);

		if ( keystone )
		{
			var h = s.Depth * 0.5f + s.KeystoneProud;
			var low = -s.KeystoneDrop;

			bay.Add( context.Solid( $"{id}/keystone", "Keystone", "Keystone", KeystoneCategory, context.Material( s.KeystoneMaterial ),
				kit => build.Carry( kit, curve, from, to, [new( -h, low ), new( h, low ), new( h, above ), new( -h, above )] ) ) );
		}

		var circle = s.Shape == ArchShape.Circular;

		if ( circle )
		{
			Lower( bay, build, id, curve.TurnedOver, section, ring );
		}

		if ( !s.WallAbove ) return;

		var wall = context.Material( s.WallMaterial );
		var edge = curve.At( rim );
		var high = keystone ? curve.At( above ) : edge;

		// What the wall has to go around, from the spring line to the crown.
		List<Vector2> Rim( Vector2 foot, IEnumerable<int> along, IEnumerable<int> along2 )
		{
			List<Vector2> points = [.. along.Select( i => edge[i] ), .. along2.Select( i => high[i] )];

			if ( points[0].y > spring ) points.Insert( 0, foot );

			return points;
		}

		var leftRim = Rim( opening[0], Enumerable.Range( 0, from + 1 ), Enumerable.Range( from, crown - from + 1 ) );
		var rightRim = Rim( opening[^1], Enumerable.Range( to, last - to + 1 ).Reverse(), Enumerable.Range( crown, to - crown + 1 ).Reverse() );

		bay.Add( context.Solid( $"{id}/wall_left", "Left Wall", "Spandrel", WallCategory, wall,
			kit => WallPiece( kit, build, leftRim, left, spring, build.Top, true ) ) );

		bay.Add( context.Solid( $"{id}/wall_right", "Right Wall", "Spandrel", WallCategory, wall,
			kit => WallPiece( kit, build, rightRim, right, spring, build.Top, true ) ) );

		if ( !circle ) return;

		// Below the spring line there is no keystone to go around.
		List<Vector2> underLeft = [.. edge.Take( crown + 1 ).Select( p => p.WithY( spring * 2.0f - p.y ) )];
		List<Vector2> underRight = [.. edge.Reverse().Take( crown + 1 ).Select( p => p.WithY( spring * 2.0f - p.y ) )];

		if ( underLeft.Min( p => p.y ) <= 0.0f ) return;

		bay.Add( context.Solid( $"{id}/wall_left_low", "Lower Left Wall", "Spandrel", WallCategory, wall,
			kit => WallPiece( kit, build, underLeft, left, spring, 0.0f, false ) ) );

		bay.Add( context.Solid( $"{id}/wall_right_low", "Lower Right Wall", "Spandrel", WallCategory, wall,
			kit => WallPiece( kit, build, underRight, right, spring, 0.0f, false ) ) );
	}

	/// <summary>
	/// The part of a full circle below the spring line.
	/// </summary>
	static void Lower( PartPlan bay, Build build, string id, RingCurve lower, List<Vector2> section, Material ring )
	{
		var s = build.S;
		var context = build.Context;
		var last = lower.Last;

		if ( !s.Voussoirs )
		{
			bay.Add( context.Solid( $"{id}/ring_lower", "Lower Ring", "ArchRing", RingCategory, ring,
				kit => build.Carry( kit, lower, 0, last, section ) ) );

			return;
		}

		var alternate = string.IsNullOrEmpty( s.AlternateMaterial ) ? ring : context.Material( s.AlternateMaterial );
		var stones = Math.Clamp( s.Stones - 1, 1, last );

		for ( var i = 0; i < stones; i++ )
		{
			var a = last * i / stones;
			var b = last * (i + 1) / stones;

			bay.Add( context.Solid( $"{id}/stone_lower{i:00}", $"Lower Stone {i + 1}", "Voussoir", RingCategory, i % 2 == 0 ? ring : alternate,
				kit => build.Carry( kit, lower, a, b, section ) ) );
		}
	}

	/// <summary>
	/// The wall between a rim and a flat line, out to the edge of the bay. The rim runs from the
	/// spring line to the crown. Above means the wall stands over the rim, otherwise under it.
	/// </summary>
	static void WallPiece( MeshKit kit, Build build, List<Vector2> rim, float edge, float spring, float line, bool above )
	{
		var depth = build.S.WallDepth * 0.5f;
		var crown = rim[^1].x;

		// A ring runs from left to right with its outside on the left, and so does a wall around one.
		var turned = edge < crown != above;

		if ( build.Straight )
		{
			List<Vector2> closed = [new( edge, spring ), .. rim, new( crown, line ), new( edge, line )];

			if ( turned ) closed.Reverse();

			kit.Profile( MeshKit.Separate( closed ), -depth, depth );
			return;
		}

		// On a path a wall follows the bends, so it is a fan of narrow pieces around the arch.
		var fan = Fan( rim, edge, spring, line, above );
		var near = turned ? rim.AsEnumerable().Reverse().ToArray() : [.. rim];
		var far = turned ? fan.AsEnumerable().Reverse().ToArray() : [.. fan];

		Vector3[] Row( Vector2[] line2, float y ) => [.. line2.Select( p => build.Place( p.x, y, p.y ) )];

		kit.Fan( Row( near, depth ), Row( far, depth ), Row( near, -depth ), Row( far, -depth ) );
	}

	/// <summary>
	/// For every point of a rim the place on the outline of the bay straight out from the middle
	/// of the arch, in order and never on top of each other.
	/// </summary>
	static List<Vector2> Fan( List<Vector2> rim, float edge, float spring, float line, bool above )
	{
		var crown = rim[^1].x;
		var side = MathF.Sign( edge - crown );
		var way = above ? 1.0f : -1.0f;
		var reach = MathF.Abs( edge - crown );
		var height = MathF.Abs( line - spring );
		var total = height + reach;
		var spacing = MathF.Min( FanSpacing, total / (rim.Count * 2.0f) );

		// Everything here is measured from the crown toward the edge and up from the spring line.
		var points = rim.Select( p => new Vector2( (p.x - crown) * side, (p.y - spring) * way ) ).ToArray();

		// How far along the outline, up the edge and on along the top, a line from here arrives.
		float Arrival( Vector2 from, Vector2 direction )
		{
			var toEdge = direction.x > 0.0f ? (reach - from.x) / direction.x : float.MaxValue;
			var toTop = direction.y > 0.0f ? (height - from.y) / direction.y : float.MaxValue;
			var hit = from + direction * MathF.Min( toEdge, toTop );

			return toEdge <= toTop ? hit.y : height + reach - hit.x;
		}

		var places = points.Select( p => p.x > 0.0f || p.y > 0.0f ? Arrival( Vector2.Zero, p ) : 0.0f ).ToArray();

		// A step straight outward, up the side of a keystone, has to stay on the wall side of its line.
		for ( var i = 0; i < points.Length - 1; i++ )
		{
			var step = points[i + 1] - points[i];

			if ( step.Normal.Dot( points[i].Normal ) > OutwardStep )
				places[i] = MathF.Min( places[i], Arrival( points[i], step.Normal ) - spacing );
		}

		places[0] = 0.0f;
		places[^1] = total;

		for ( var i = places.Length - 2; i > 0; i-- )
			places[i] = MathF.Min( places[i], places[i + 1] - spacing );

		for ( var i = 1; i < places.Length - 1; i++ )
			places[i] = MathF.Max( places[i], places[i - 1] + spacing );

		// The corner of the bay has to be a point of the outline.
		var corner = Enumerable.Range( 1, places.Length - 2 ).OrderBy( i => MathF.Abs( places[i] - height ) ).First();

		places[corner] = height;

		return [.. places.Select( x => x <= height
			? new Vector2( edge, spring + way * x )
			: new Vector2( edge - side * (x - height), line ) )];
	}
}

/// <summary>
/// The same arches standing along a drawn path or a spiral, bent with it.
/// </summary>
public sealed class CurvedArchGenerator : ArchGenerator
{
	public override string Id => "curvedarch";
	public override string Title => "Curved Arch";
	public override Type SettingsType => typeof( CurvedArchSettings );

	protected override PathTrack Route( ArchSettings settings )
	{
		var s = (CurvedArchSettings)settings;

		if ( s.Path is null || s.Path.SegmentCount == 0 ) return null;

		var reach = s.Depth * 0.5f + s.KeystoneProud + MathF.Max( s.BaseOverhang, s.ImpostOverhang );

		return s.Path.Track( s.PathDetail, FrameMode.Upright, 4.0f, s.Climb ).Plumbed().Reach( reach, reach );
	}
}