editor/AdvancedMappingTools/Core/WindowKit.cs
namespace AdvancedMappingTools;

/// <summary>
/// How the four members of a window frame meet. New kinds go at the end.
/// </summary>
public enum FrameCorner
{
	[Title( "Butt Joint" )] Butt,
	Mitred,
	Overlapping,
	[Title( "Reinforced (Gusset Plates)" )] Reinforced,
	Rounded,
	Chamfered,
	[Title( "Decorative Corner Blocks" )] Decorative,
	[Title( "Timber Joint (Pegged)" )] TimberJoint,
	Welded
}

/// <summary>
/// The cross section of a frame member. New kinds go at the end.
/// </summary>
public enum FrameProfile
{
	Rectangular,
	Slim,
	Deep,
	Rounded,
	Chamfered,
	Bevelled,
	Stepped,
	Classical,
	Timber,
	[Title( "Steel (T Section)" )] Steel,
	Aluminium,
	[Title( "Multi Layer" )] MultiLayer,
	Flush,
	Recessed,
	Projecting
}

/// <summary>
/// How much of a window is really built. New kinds go at the end.
/// </summary>
public enum WindowDetail
{
	Basic,
	Standard,
	Detailed,
	[Title( "High Detail" )] High
}

public enum Glazing
{
	Single,
	Double,
	Triple
}

/// <summary>
/// The outline of a window. Every shape but the rectangle is framed as one seamless ring.
/// </summary>
public enum WindowShape
{
	Rectangle,
	[Title( "Round / Oval" )] Round,
	[Title( "Half Round" )] HalfRound,
	Triangle,
	Octagon
}

/// <summary>
/// Everything that decides how one window is put together.
/// </summary>
public sealed class WindowSpec
{
	/// <summary>Width of a frame member, seen from the front.</summary>
	public float Width { get; set; } = 3.0f;
	/// <summary>Depth of the frame, from its front to its back.</summary>
	public float Depth { get; set; } = 4.0f;
	/// <summary>Size of the moulding on the edge of the profile.</summary>
	public float Edge { get; set; } = 0.75f;
	/// <summary>Radius or cut of rounded and chamfered corners, size of corner blocks above the frame width.</summary>
	public float CornerSize { get; set; } = 2.0f;
	public FrameProfile Profile { get; set; }
	public FrameCorner Corner { get; set; } = FrameCorner.Mitred;
	public WindowDetail Detail { get; set; } = WindowDetail.Standard;
	public bool Glass { get; set; } = true;
	public Glazing Glazing { get; set; }
	/// <summary>Where the glass sits in the depth of the frame, 0 at the front and 1 at the back.</summary>
	public float GlassInset { get; set; } = 0.5f;
	public int Columns { get; set; } = 1;
	public int Rows { get; set; } = 1;
	public float BarWidth { get; set; } = 1.0f;
	public Material FrameMaterial { get; set; }
	public Material GlassMaterial { get; set; }
	/// <summary>Gaskets, bolts and other small parts. The frame material when not set.</summary>
	public Material DetailMaterial { get; set; }
	/// <summary>An arched head. The frame then runs as one seamless ring along the arch, whatever the corner joint.</summary>
	public ArchStyle Arch { get; set; } = ArchStyle.Flat;
	/// <summary>Rise of the arch above its springing line; 0 takes the usual rise of the style.</summary>
	public float ArchRise { get; set; }
	public int ArchSegments { get; set; } = 12;
	public WindowShape Shape { get; set; } = WindowShape.Rectangle;
}

/// <summary>
/// Builds windows from parts that really fit: frame members with a profile, corners that are
/// joined and not stacked, glass that sits in a rebate, beads, gaskets and glazing bars.
/// A window is built in the XZ plane of the kit's frame: +X to the right, +Z up, its front
/// looks along -Y. The rectangle given is the hole, the frame stands inside its edges.
/// </summary>
/// <summary>
/// The pieces of a window, to build them into separate parts.
/// </summary>
[Flags]
public enum WindowPieces
{
	Frame = 1,
	Glass = 2,
	Bars = 4,
	All = Frame | Glass | Bars
}

public static class WindowKit
{
	const float GasketSize = 0.12f;
	const float ButtStep = 0.12f;

	public static bool IsSwept( FrameCorner corner ) =>
		corner is FrameCorner.Mitred or FrameCorner.Welded or FrameCorner.Rounded or FrameCorner.Chamfered;

	/// <summary>
	/// What <see cref="Build"/> made, for the parts a caller puts around it.
	/// </summary>
	public readonly record struct Result( float Width, float Depth, float GlassFront, float GlassBack );

	sealed class Job
	{
		public MeshKit Kit;
		public WindowSpec S;
		public float X0, X1, Z0, Z1, Front;
		public float W, D, E;
		public bool Swept, Steel, Glazed, Arched, Shaped;
		/// <summary>Where an arched head springs from its straight sides.</summary>
		public float Spring;
		public int Round;
		/// <summary>Front of the inner face, front of the glass, thickness of the glass.</summary>
		public float Vf, Gv, Gt;
		/// <summary>Depth of the rebate the glass stands in, size of beads, in-plane size of corner blocks.</summary>
		public float R, Bead, Block;
		public Vector2[] Outer, Inner;
		public Material Frame, Glass, Small;
	}

	public static Vector2[] Rect( float x0, float x1, float z0, float z1 ) =>
		[new( x0, z0 ), new( x1, z0 ), new( x1, z1 ), new( x0, z1 )];

	/// <summary>
	/// A closed outline that runs counter clockwise seen from the front, moved inward by the same
	/// distance everywhere. Corners are mitred, so a rectangle stays one.
	/// </summary>
	public static Vector2[] Inset( IReadOnlyList<Vector2> path, float distance )
	{
		var count = path.Count;
		var result = new Vector2[count];

		for ( var i = 0; i < count; i++ )
		{
			var before = (path[i] - path[(i + count - 1) % count]).Normal;
			var after = (path[(i + 1) % count] - path[i]).Normal;
			var n0 = new Vector2( -before.y, before.x );
			var n1 = new Vector2( -after.y, after.x );

			result[i] = path[i] + (n0 + n1) * (distance / MathF.Max( 1.0f + n0.Dot( n1 ), 0.3f ));
		}

		return result;
	}

	/// <summary>
	/// A closed band between two outlines with the same number of points, as one body without
	/// a seam. The shape is its cross section: x from the first outline (0) to the second one
	/// (<paramref name="width"/>), y the depth behind <paramref name="front"/>.
	/// </summary>
	public static void Ring( MeshKit kit, IReadOnlyList<Vector2> from, IReadOnlyList<Vector2> to, float width, IReadOnlyList<Vector2> shape, float front )
	{
		var rings = new List<Vector3[]>( shape.Count );

		foreach ( var p in Wound( shape ) )
		{
			var ring = new Vector3[from.Count];

			for ( var i = 0; i < ring.Length; i++ )
			{
				var q = from[i] + (to[i] - from[i]) * (p.x / width);
				ring[i] = new Vector3( q.x, front + p.y, q.y );
			}

			rings.Add( ring );
		}

		kit.Sweep( rings, true );
	}

	/// <summary>
	/// A straight member with this cross section from a to b. Its x runs to the left of that way.
	/// </summary>
	public static void Bar( MeshKit kit, Vector2 a, Vector2 b, IReadOnlyList<Vector2> shape, float front )
	{
		var along = (b - a).Normal;
		var inward = new Vector2( -along.y, along.x );
		var rings = new List<Vector3[]>( shape.Count );

		foreach ( var p in Wound( shape ) )
		{
			var q0 = a + inward * p.x;
			var q1 = b + inward * p.x;
			rings.Add( [new Vector3( q0.x, front + p.y, q0.y ), new Vector3( q1.x, front + p.y, q1.y )] );
		}

		kit.Sweep( rings, false );
	}

	/// <summary>
	/// A cross section always runs the same way round, whichever way it was drawn.
	/// </summary>
	static IEnumerable<Vector2> Wound( IReadOnlyList<Vector2> shape )
	{
		var area = 0.0f;

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

		return area > 0.0f ? shape.Reverse() : shape;
	}

	/// <summary>
	/// A closed body between two rings with the same number of corners, whichever way they run.
	/// </summary>
	public static void Loft( MeshKit kit, IReadOnlyList<Vector3> near, IReadOnlyList<Vector3> far )
	{
		var normal = Vector3.Zero;
		var a = Vector3.Zero;
		var b = Vector3.Zero;

		for ( var i = 0; i < near.Count; i++ )
		{
			normal += Vector3.Cross( near[i] - near[0], near[(i + 1) % near.Count] - near[0] );
			a += near[i];
			b += far[i];
		}

		if ( normal.Dot( b - a ) > 0.0f )
		{
			near = [.. near.Reverse()];
			far = [.. far.Reverse()];
		}

		kit.BeginPart();
		kit.Face( [.. near] );
		kit.Face( [.. far.Reverse()] );

		for ( var i = 0; i < near.Count; i++ )
		{
			var j = (i + 1) % near.Count;
			kit.Face( near[i], far[i], far[j], near[j] );
		}
	}

	static void Slab( MeshKit kit, float x0, float x1, float z0, float z1, float y0, float y1 )
	{
		if ( x1 - x0 < 0.01f || z1 - z0 < 0.01f || y1 - y0 < 0.01f ) return;

		kit.Box( new Vector3( x0, y0, z0 ), new Vector3( x1, y1, z1 ) );
	}

	/// <summary>
	/// The front of a profile from its outer edge (x 0) to its inner edge (x width). y is the
	/// depth behind the front of the frame.
	/// </summary>
	public static List<Vector2> FrontLine( FrameProfile profile, float w, float e, int round )
	{
		var line = new List<Vector2>();

		switch ( profile )
		{
			case FrameProfile.Rounded:
				line.Add( new( 0, 0 ) );
				for ( var i = 0; i <= round; i++ )
				{
					var t = MathF.PI * 0.5f * i / round;
					line.Add( new( w - e + MathF.Sin( t ) * e, e - MathF.Cos( t ) * e ) );
				}
				break;
			case FrameProfile.Chamfered:
				line.AddRange( [new( 0, 0 ), new( w - e, 0 ), new( w, e )] );
				break;
			case FrameProfile.Bevelled:
				line.AddRange( [new( 0, 0 ), new( w * 0.15f, 0 ), new( w, e )] );
				break;
			case FrameProfile.Stepped:
				line.AddRange( [new( 0, 0 ), new( w - e, 0 ), new( w - e, e * 0.5f ), new( w - e * 0.5f, e * 0.5f ), new( w - e * 0.5f, e ), new( w, e )] );
				break;
			case FrameProfile.Classical:
			{
				var a = w * 0.3f;
				var b = w * 0.15f;
				var s = e * 0.3f;
				line.AddRange( [new( 0, 0 ), new( a, 0 )] );
				for ( var i = 0; i <= round; i++ )
				{
					var t = MathF.PI * 0.5f * i / round;
					line.Add( new( a + MathF.Sin( t ) * (w - b - a), e - MathF.Cos( t ) * (e - s) ) );
				}
				line.Add( new( w, e ) );
				break;
			}
			case FrameProfile.Timber:
				line.AddRange( [new( 0, e * 0.5f ), new( e * 0.5f, 0 ), new( w - e * 0.5f, 0 ), new( w, e * 0.5f )] );
				break;
			case FrameProfile.Aluminium:
				line.AddRange( [new( 0, 0 ), new( w * 0.4f, 0 ), new( w * 0.4f, e * 0.35f ), new( w * 0.55f, e * 0.35f ), new( w * 0.55f, 0 ), new( w - e * 0.25f, 0 ), new( w, e * 0.25f )] );
				break;
			case FrameProfile.MultiLayer:
				line.AddRange( [new( 0, 0 ), new( w * 0.4f, 0 ), new( w * 0.4f, e * 0.5f ), new( w * 0.75f, e * 0.5f ), new( w * 0.75f, e ), new( w, e )] );
				break;
			case FrameProfile.Recessed:
				line.AddRange( [new( 0, 0 ), new( w * 0.2f, 0 ), new( w * 0.2f, e ), new( w, e )] );
				break;
			case FrameProfile.Projecting:
				line.AddRange( [new( 0, -e ), new( w * 0.5f, -e ), new( w * 0.5f, 0 ), new( w, 0 )] );
				break;
			default:
				line.AddRange( [new( 0, 0 ), new( w, 0 )] );
				break;
		}

		return line;
	}

	static Job Plan( MeshKit kit, float x0, float x1, float z0, float z1, float front, WindowSpec s )
	{
		var job = new Job { Kit = kit, S = s, X0 = x0, X1 = x1, Z0 = z0, Z1 = z1, Front = front };
		var span = MathF.Min( x1 - x0, z1 - z0 );

		job.W = MathF.Max( MathF.Min( s.Width * (s.Profile == FrameProfile.Slim ? 0.55f : 1.0f), span * 0.25f ), 0.2f );
		job.D = MathF.Max( s.Depth * (s.Profile == FrameProfile.Deep ? 1.75f : s.Profile == FrameProfile.Flush ? 0.6f : 1.0f), 0.2f );
		job.E = MathF.Max( MathF.Min( s.Edge, MathF.Min( job.W * 0.45f, job.D * 0.3f ) ), 0.02f );
		var rise = s.Arch == ArchStyle.Flat ? 0.0f : s.ArchRise > 0.0f ? s.ArchRise : MeshKit.ArchRise( s.Arch, x1 - x0 );
		// The inner line is an arch of its own, lower by the frame width: it needs some rise left.
		if ( rise > 0.0f ) rise = MathF.Min( rise, z1 - z0 - job.W * 2.0f - 0.5f );
		job.Shaped = s.Shape != WindowShape.Rectangle;
		job.Arched = !job.Shaped && rise > job.W + 0.4f;
		job.Swept = IsSwept( s.Corner ) || job.Arched || job.Shaped;
		job.Steel = s.Profile == FrameProfile.Steel;
		job.Glazed = s.Glass;
		job.Round = s.Detail switch { WindowDetail.Basic => 2, WindowDetail.Standard => 3, WindowDetail.Detailed => 5, _ => 8 };
		job.Frame = s.FrameMaterial ?? kit.Material;
		job.Glass = s.GlassMaterial ?? Material.Load( "materials/glass/glass_a.vmat" );
		job.Small = s.DetailMaterial ?? job.Frame;

		var w = job.W;
		var d = job.D;
		var line = FrontLine( s.Profile, w, job.E, job.Round );

		job.Vf = job.Steel ? 0.0f : line[^1].y;
		job.Bead = MathF.Min( MathF.Min( 0.6f, d * 0.15f ), w * 0.5f );
		job.Gt = MathF.Min( s.Glazing switch { Glazing.Single => 0.25f, Glazing.Double => 0.75f, _ => 1.25f }, d * 0.25f );

		if ( job.Steel )
		{
			// A flange in front and a web on the outside: the glass stands behind the flange.
			job.Gv = MathF.Max( MathF.Min( job.E, d * 0.25f ), 0.15f );
			job.R = w - MathF.Min( job.Gv, w * 0.4f );
		}
		else
		{
			job.Gv = Math.Clamp( d * s.GlassInset, job.Vf + job.Bead, MathF.Max( d - job.Gt - d * 0.1f, job.Vf + job.Bead ) );
			job.R = s.Detail == WindowDetail.Basic || !job.Swept ? 0.0f : MathF.Max( MathF.Min( w * 0.35f, 1.0f ), 0.1f );
		}

		job.Block = s.Corner switch
		{
			FrameCorner.Overlapping => w * 1.15f,
			FrameCorner.Decorative => w + MathF.Min( MathF.Max( s.CornerSize, 0.0f ) * 0.5f + w * 0.2f, span * 0.1f ),
			_ => w
		};

		if ( job.Shaped )
		{
			(job.Outer, job.Inner) = Outlines( s, x0, x1, z0, z1, w );
		}
		else if ( job.Arched )
		{
			// The head follows the arch. The inside is the same arch, narrower by the frame width on
			// both sides and lower by it at the crown, from the same springing line. An offset line
			// would cross itself under sharp crowns such as an ogee.
			job.Spring = z1 - rise;
			var curve = MeshKit.ArchCurve( x0, x1, job.Spring, rise, s.Arch, s.ArchSegments );
			var inside = MeshKit.ArchCurve( x0 + w, x1 - w, job.Spring, rise - w, s.Arch, s.ArchSegments );
			job.Outer = [new( x0, z0 ), new( x1, z0 ), .. curve.Reverse()];
			job.Inner = [new( x0 + w, z0 + w ), new( x1 - w, z0 + w ), .. inside.Reverse()];
		}
		else if ( job.Swept )
		{
			var outer = Rect( x0, x1, z0, z1 );
			var inner = Rect( x0 + w, x1 - w, z0 + w, z1 - w );
			var size = MathF.Min( MathF.Max( s.CornerSize, 0.05f ), (span - w * 2.0f) * 0.45f );

			if ( s.Corner == FrameCorner.Rounded )
			{
				var steps = job.Round + 1;
				var o = new List<Vector2>();
				var n = new List<Vector2>();

				for ( var c = 0; c < 4; c++ )
				{
					// Centre of the arc and the angle it starts at, going round counter clockwise.
					var centre = inner[c] + new Vector2( c is 0 or 3 ? size : -size, c < 2 ? size : -size );
					var start = MathF.PI * (1.0f + c * 0.5f);

					for ( var i = 0; i <= steps; i++ )
					{
						var t = start + MathF.PI * 0.5f * i / steps;
						o.Add( outer[c] );
						n.Add( centre + new Vector2( MathF.Cos( t ), MathF.Sin( t ) ) * size );
					}
				}

				outer = [.. o];
				inner = [.. n];
			}
			else if ( s.Corner == FrameCorner.Chamfered )
			{
				var o = new List<Vector2>();
				var n = new List<Vector2>();

				for ( var c = 0; c < 4; c++ )
				{
					var before = (inner[c] - inner[(c + 3) % 4]).Normal;
					var after = (inner[(c + 1) % 4] - inner[c]).Normal;

					o.AddRange( [outer[c], outer[c]] );
					n.AddRange( [inner[c] - before * size, inner[c] + after * size] );
				}

				outer = [.. o];
				inner = [.. n];
			}

			job.Outer = outer;
			job.Inner = inner;
		}

		return job;
	}

	/// <summary>
	/// The cross section of a frame member: outside at x 0, the opening at x width.
	/// </summary>
	static List<Vector2> Section( Job job, float? lowered = null )
	{
		var w = job.W;
		var d = job.D;
		var shape = new List<Vector2> { new( 0, d ) };

		foreach ( var p in FrontLine( job.S.Profile, w, job.E, job.Round ) )
			shape.Add( new( p.x, lowered is float low ? MathF.Max( p.y, MathF.Min( low, job.Gv - job.Bead ) ) : p.y ) );

		if ( job.Steel )
		{
			shape.AddRange( [new( w, job.Gv ), new( w - job.R, job.Gv ), new( w - job.R, d )] );
		}
		else if ( !job.Glazed || job.R <= 0.0f )
		{
			shape.Add( new( w, d ) );
		}
		else if ( job.S.Detail == WindowDetail.Standard )
		{
			// The glass stands in a groove.
			shape.AddRange( [new( w, job.Gv ), new( w - job.R, job.Gv ), new( w - job.R, job.Gv + job.Gt ), new( w, job.Gv + job.Gt ), new( w, d )] );
		}
		else
		{
			// A rebate, closed by a bead of its own behind the glass.
			shape.AddRange( [new( w, job.Gv ), new( w - job.R, job.Gv ), new( w - job.R, d )] );
		}

		return MeshKit.Separate( shape );
	}

	static Vector2[] Sides( Job job, int side )
	{
		var b = job.Block;

		return side switch
		{
			0 => [new( job.X0 + b, job.Z0 ), new( job.X1 - b, job.Z0 )],
			1 => [new( job.X1, job.Z0 + b ), new( job.X1, job.Z1 - b )],
			2 => [new( job.X1 - b, job.Z1 ), new( job.X0 + b, job.Z1 )],
			_ => [new( job.X0, job.Z1 - b ), new( job.X0, job.Z0 + b )]
		};
	}

	/// <summary>
	/// A window in the hole from x0 to x1 and z0 to z1. Its front lies at y = front, its depth
	/// runs back from there.
	/// </summary>
	public static Result Build( MeshKit kit, float x0, float x1, float z0, float z1, float front, WindowSpec s, WindowPieces pieces = WindowPieces.All )
	{
		if ( x1 - x0 < 1.0f || z1 - z0 < 1.0f ) return default;

		var job = Plan( kit, x0, x1, z0, z1, front, s );
		var material = kit.Material;
		var w = job.W;
		var d = job.D;
		var detailed = s.Detail >= WindowDetail.Detailed;

		kit.Material = job.Frame;
		kit.BeginAssembly();

		if ( (pieces & WindowPieces.Frame) == 0 )
		{
		}
		else if ( job.Swept )
		{
			Ring( kit, job.Outer, job.Inner, w, Section( job ), front );

			if ( job.Glazed && detailed && !job.Steel && job.R > 0.0f )
			{
				// The bead that holds the glass, a part of its own.
				var back = MathF.Min( job.Gv + job.Gt + job.Bead, d );
				Ring( kit, job.Outer, job.Inner, w, [new( w - job.R, job.Gv + job.Gt ), new( w, job.Gv + job.Gt ), new( w, back ), new( w - job.R, back )], front );
			}

			if ( job.Glazed && detailed && job.Steel )
			{
				// Putty behind the glass.
				var u = w - job.R;
				var size = MathF.Min( job.Bead, job.R * 0.5f );
				kit.Material = job.Small;
				Ring( kit, job.Outer, job.Inner, w, [new( u, job.Gv + job.Gt ), new( u + size, job.Gv + job.Gt ), new( u, job.Gv + job.Gt + size )], front );
				kit.Material = job.Frame;
			}

			if ( s.Corner == FrameCorner.Welded && s.Detail != WindowDetail.Basic && !job.Arched && !job.Shaped )
				Welds( job );
		}
		else
		{
			Blocks( job );
		}

		if ( job.Glazed && (pieces & WindowPieces.Glass) != 0 )
			Glaze( job );

		if ( (pieces & WindowPieces.Bars) != 0 )
			Bars( job );

		kit.EndAssembly();
		kit.Material = material;

		return new Result( w, d, front + job.Gv, front + job.Gv + job.Gt );
	}

	static void Blocks( Job job )
	{
		var kit = job.Kit;
		var s = job.S;
		var w = job.W;
		var b = job.Block;
		var top = FrontLine( s.Profile, w, job.E, job.Round ).Min( p => p.y );
		var proud = s.Corner switch
		{
			FrameCorner.Overlapping => MathF.Min( job.D * 0.15f, 0.5f ),
			FrameCorner.Decorative => MathF.Min( job.D * 0.2f, 0.75f ),
			_ => 0.0f
		};
		var y0 = job.Front + top - proud;
		var y1 = job.Front + job.D + (s.Corner == FrameCorner.Overlapping ? proud : 0.0f);

		for ( var side = 0; side < 4; side++ )
		{
			var ends = Sides( job, side );
			if ( (ends[1] - ends[0]).Length < 0.05f ) continue;

			// On a butt joint the rails stand back a little behind the stiles that run through.
			Bar( kit, ends[0], ends[1], Section( job, s.Corner == FrameCorner.Butt && side % 2 == 0 ? top + ButtStep : null ), job.Front );
		}

		var corners = new (float X, float Z, int Sx, int Sz)[] { (job.X0, job.Z0, 1, 1), (job.X1, job.Z0, -1, 1), (job.X1, job.Z1, -1, -1), (job.X0, job.Z1, 1, -1) };

		foreach ( var (x, z, sx, sz) in corners )
		{
			var xa = MathF.Min( x, x + sx * b );
			var za = MathF.Min( z, z + sz * b );

			kit.Material = job.Frame;
			Slab( kit, xa, xa + b, za, za + b, y0, y1 );

			if ( s.Detail == WindowDetail.Basic ) continue;

			var centre = new Vector2( xa + b * 0.5f, za + b * 0.5f );

			if ( s.Corner == FrameCorner.Decorative )
			{
				// A rosette: a low pyramid with a button on it.
				var r0 = b * 0.38f;
				var r1 = b * 0.2f;
				var rise = MathF.Min( b * 0.15f, 0.5f );
				Loft( kit, Square( centre, r0, y0 ), Square( centre, r1, y0 - rise ) );
				if ( s.Detail >= WindowDetail.Detailed )
					Slab( kit, centre.x - r1 * 0.5f, centre.x + r1 * 0.5f, centre.y - r1 * 0.5f, centre.y + r1 * 0.5f, y0 - rise * 1.6f, y0 - rise );
			}
			else if ( s.Corner == FrameCorner.TimberJoint )
			{
				// Two pegs through the joint.
				var peg = MathF.Min( b * 0.14f, 0.4f );
				foreach ( var t in new[] { 0.3f, 0.7f } )
				{
					var p = new Vector2( xa + b * t, za + b * (1.0f - t) );
					Slab( kit, p.x - peg, p.x + peg, p.y - peg, p.y + peg, y0 - MathF.Min( peg, 0.25f ), y0 );
				}
			}
			else if ( s.Corner == FrameCorner.Reinforced )
			{
				// A gusset plate over the joint, bolted to both members.
				var leg = b * 2.0f;
				var plate = 0.15f;
				var corner = new Vector3( x, y0, z );
				var along = new Vector3( sx * leg, 0, 0 );
				var up = new Vector3( 0, 0, sz * leg );
				var lift = new Vector3( 0, -plate, 0 );

				kit.Material = job.Small;
				Loft( kit, [corner + lift, corner + along + lift, corner + up + lift], [corner, corner + along, corner + up] );

				if ( s.Detail >= WindowDetail.Detailed )
				{
					var bolt = MathF.Min( b * 0.12f, 0.3f );
					foreach ( var (u, v) in new[] { (0.5f, 0.5f), (1.35f, 0.3f), (0.3f, 1.35f) } )
					{
						var p = new Vector2( x + sx * b * u, z + sz * b * v );
						Slab( kit, p.x - bolt, p.x + bolt, p.y - bolt, p.y + bolt, y0 - plate - bolt * 0.6f, y0 - plate );
					}
				}
			}
		}

		kit.Material = job.Frame;

		if ( !job.Glazed || s.Detail == WindowDetail.Basic ) return;

		// Without a groove in the members the glass is held by a stop in front and a bead behind.
		var clear = Clear( job );
		var bead = job.Bead;
		var inner = Inset( clear, bead );

		Ring( kit, clear, inner, bead, [new( 0, job.Gv - bead ), new( bead, job.Gv - bead ), new( bead, job.Gv ), new( 0, job.Gv )], job.Front );

		var back = MathF.Min( job.Gv + job.Gt + bead, job.D );
		if ( back - job.Gv - job.Gt > 0.02f )
			Ring( kit, clear, inner, bead, [new( 0, job.Gv + job.Gt ), new( bead, job.Gv + job.Gt ), new( bead, back ), new( 0, back )], job.Front );
	}

	static Vector3[] Square( Vector2 centre, float half, float y ) =>
		[new( centre.x - half, y, centre.y - half ), new( centre.x + half, y, centre.y - half ), new( centre.x + half, y, centre.y + half ), new( centre.x - half, y, centre.y + half )];

	/// <summary>
	/// The outline of what stays open between the members and the corner blocks.
	/// </summary>
	static Vector2[] Clear( Job job )
	{
		var w = job.W;
		var b = job.Block;
		float x0 = job.X0, x1 = job.X1, z0 = job.Z0, z1 = job.Z1;

		if ( job.Swept ) return MeshKit.Separate( job.Inner ).ToArray();
		if ( b - w < 0.01f ) return Rect( x0 + w, x1 - w, z0 + w, z1 - w );

		return
		[
			new( x0 + b, z0 + w ), new( x1 - b, z0 + w ), new( x1 - b, z0 + b ), new( x1 - w, z0 + b ),
			new( x1 - w, z1 - b ), new( x1 - b, z1 - b ), new( x1 - b, z1 - w ), new( x0 + b, z1 - w ),
			new( x0 + b, z1 - b ), new( x0 + w, z1 - b ), new( x0 + w, z0 + b ), new( x0 + b, z0 + b )
		];
	}

	static void Welds( Job job )
	{
		var kit = job.Kit;
		var w = job.W;
		var half = MathF.Min( 0.2f, w * 0.12f );
		var rise = MathF.Min( 0.12f, half );
		var line = FrontLine( job.S.Profile, w, job.E, job.Round );
		var corners = new (float X, float Z, int Sx, int Sz)[] { (job.X0, job.Z0, 1, 1), (job.X1, job.Z0, -1, 1), (job.X1, job.Z1, -1, -1), (job.X0, job.Z1, 1, -1) };

		kit.Material = job.Small;

		foreach ( var (x, z, sx, sz) in corners )
		{
			// A seam along the mitre, on the flat faces of the profile. To either side of the mitre the
			// face belongs to the bar there, a little nearer its outer edge, so a seam only lies flat
			// where the profile is level that far back.
			var along = new Vector2( sx, sz );
			var across = new Vector2( -sz, sx ).Normal * half;
			var side = half * 0.70711f;

			for ( var i = 0; i + 1 < line.Count; i++ )
			{
				var from = line[i];
				var to = line[i + 1];

				if ( MathF.Abs( to.y - from.y ) > 0.001f || to.x - from.x - side < 0.1f ) continue;

				var a = new Vector2( x, z ) + along * (from.x + side);
				var b = new Vector2( x, z ) + along * to.x;
				var y = job.Front + from.y;

				Loft( kit,
					[P( a - across, y - rise ), P( b - across, y - rise ), P( b + across, y - rise ), P( a + across, y - rise )],
					[P( a - across, y ), P( b - across, y ), P( b + across, y ), P( a + across, y )] );
			}
		}

		kit.Material = job.Frame;
	}

	static Vector3 P( Vector2 p, float y ) => new( p.x, y, p.y );

	static void Glaze( Job job )
	{
		var kit = job.Kit;
		var s = job.S;
		var w = job.W;
		var y0 = job.Front + job.Gv;
		var y1 = y0 + job.Gt;
		var panes = s.Glazing switch { Glazing.Single => 1, Glazing.Double => 2, _ => 3 };
		var split = job.Swept && s.Detail >= WindowDetail.Detailed && panes > 1 && job.Gt > 0.5f;

		kit.Material = job.Glass;

		if ( job.Swept )
		{
			var u = (w - job.R) / w;
			var outline = MeshKit.Separate( job.Outer.Select( ( o, i ) => o + (job.Inner[i] - o) * u ) );

			if ( split )
			{
				// Real panes with the gap between them.
				var pane = job.Gt / (panes * 2 - 1);
				for ( var i = 0; i < panes; i++ )
					kit.Profile( outline, y0 + pane * i * 2, y0 + pane * (i * 2 + 1) );

				kit.Material = job.Small;
				for ( var i = 0; i + 1 < panes; i++ )
				{
					var from = job.Gv + pane * (i * 2 + 1);
					Ring( kit, job.Outer, job.Inner, w, [new( w - job.R, from ), new( w + GasketSize, from ), new( w + GasketSize, from + pane ), new( w - job.R, from + pane )], job.Front );
				}
			}
			else
			{
				kit.Profile( outline, y0, y1 );
			}

			if ( s.Detail >= WindowDetail.Detailed && s.Columns * s.Rows <= 1 && job.Gv - job.Vf > GasketSize * 2.0f )
			{
				// The gasket between the frame and the glass.
				var clear = Clear( job );
				kit.Material = job.Small;
				Ring( kit, clear, Inset( clear, GasketSize ), GasketSize, [new( 0, job.Gv - GasketSize ), new( GasketSize, job.Gv - GasketSize ), new( GasketSize, job.Gv ), new( 0, job.Gv )], job.Front );
			}
		}
		else
		{
			var b = job.Block;
			float x0 = job.X0, x1 = job.X1, z0 = job.Z0, z1 = job.Z1;

			// One pane, cut out around the corner blocks. Its pieces meet on whole faces and become one body.
			Slab( kit, x0 + b, x1 - b, z0 + b, z1 - b, y0, y1 );
			Slab( kit, x0 + w, x0 + b, z0 + b, z1 - b, y0, y1 );
			Slab( kit, x1 - b, x1 - w, z0 + b, z1 - b, y0, y1 );
			Slab( kit, x0 + b, x1 - b, z0 + w, z0 + b, y0, y1 );
			Slab( kit, x0 + b, x1 - b, z1 - b, z1 - w, y0, y1 );
		}

		kit.Material = job.Frame;
	}

	/// <summary>
	/// Glazing bars. The upright ones run through, the level ones stand between them.
	/// </summary>
	static void Bars( Job job )
	{
		var s = job.S;
		var columns = Math.Max( s.Columns, 1 );
		var rows = Math.Max( s.Rows, 1 );
		if ( columns * rows <= 1 ) return;

		var kit = job.Kit;
		var beads = !job.Swept && job.Glazed && s.Detail != WindowDetail.Basic;
		var edge = job.W + (beads ? job.Bead : 0.0f);
		float x0 = job.X0 + edge, x1 = job.X1 - edge, z0 = job.Z0 + edge, z1 = job.Z1 - edge;
		var half = MathF.Min( s.BarWidth, MathF.Min( (x1 - x0) / columns, (z1 - z0) / rows ) * 0.4f ) * 0.5f;
		if ( half < 0.05f ) return;

		var layers = new List<(float From, float To)>();
		var depth = beads ? job.Bead : MathF.Min( job.Bead, job.Gv - job.Vf );
		// Under an arch the level bars stay in the straight part, the upright ones end below the curve.
		var straight = job.Arched ? MathF.Min( job.Spring, z1 ) : z1;

		if ( job.Glazed )
		{
			if ( depth > 0.05f ) layers.Add( (job.Gv - depth, job.Gv) );
			if ( !job.Steel && job.D - job.Gv - job.Gt > 0.05f ) layers.Add( (job.Gv + job.Gt, MathF.Min( job.Gv + job.Gt + job.Bead, job.D )) );
		}
		else
		{
			layers.Add( (MathF.Max( job.Vf, job.D * 0.5f - job.Bead ), MathF.Min( job.D, job.D * 0.5f + job.Bead )) );
		}

		kit.Material = job.Frame;

		foreach ( var (from, to) in layers )
		{
			var y0 = job.Front + from;
			var y1 = job.Front + to;
			var cuts = new List<float> { x0 };

			for ( var c = 1; c < columns; c++ )
			{
				var x = x0 + (x1 - x0) * c / columns;
				var top = job.Arched || job.Shaped ? MathF.Min( Under( job.Inner, x - half ), Under( job.Inner, x + half ) ) - 0.02f : z1;
				var bottom = job.Shaped ? MathF.Max( Across( job.Inner, x - half, true ).Low, Across( job.Inner, x + half, true ).Low ) + 0.02f : z0;
				Slab( kit, x - half, x + half, bottom, top, y0, y1 );
				cuts.Add( x - half );
				cuts.Add( x + half );
			}

			cuts.Add( x1 );

			for ( var r = 1; r < rows; r++ )
			{
				var z = z0 + (straight - z0) * r / rows;
				var left = x0;
				var right = x1;
				if ( job.Shaped )
				{
					// A level bar ends where the outline crosses it, at both its edges.
					var below = Across( job.Inner, z - half, false );
					var above = Across( job.Inner, z + half, false );
					left = MathF.Max( below.Low, above.Low ) + 0.02f;
					right = MathF.Min( below.High, above.High ) - 0.02f;
				}

				for ( var i = 0; i + 1 < cuts.Count; i += 2 )
					Slab( kit, MathF.Max( cuts[i], left ), MathF.Min( cuts[i + 1], right ), z - half, z + half, y0, y1 );
			}
		}
	}

	/// <summary>
	/// Where a straight line crosses a closed outline: upright through x (<paramref name="upright"/>)
	/// or level through z. The lowest and highest crossing.
	/// </summary>
	static (float Low, float High) Across( IReadOnlyList<Vector2> outline, float at, bool upright )
	{
		var low = float.MaxValue;
		var high = float.MinValue;
		for ( var i = 0; i < outline.Count; i++ )
		{
			var a = outline[i];
			var b = outline[(i + 1) % outline.Count];
			var (ka, kb, va, vb) = upright ? (a.x, b.x, a.y, b.y) : (a.y, b.y, a.x, b.x);
			if ( (at < ka || at > kb) && (at < kb || at > ka) || MathF.Abs( kb - ka ) < 0.0001f ) continue;
			var v = va + (vb - va) * (at - ka) / (kb - ka);
			low = MathF.Min( low, v );
			high = MathF.Max( high, v );
		}

		return low > high ? (0.0f, 0.0f) : (low, high);
	}

	/// <summary>
	/// Outer and inner outline of a shaped window in the hole x0..x1, z0..z1, with the same number
	/// of points, the inner one the frame width inside.
	/// </summary>
	static (Vector2[] Outer, Vector2[] Inner) Outlines( WindowSpec s, float x0, float x1, float z0, float z1, float w )
	{
		var cx = (x0 + x1) * 0.5f;
		var rx = (x1 - x0) * 0.5f;
		var steps = Math.Clamp( s.ArchSegments, 6, 32 ) * 2;
		switch ( s.Shape )
		{
			case WindowShape.Round:
			{
				// An ellipse filling the hole; a circle in a square one.
				var cz = (z0 + z1) * 0.5f;
				var rz = (z1 - z0) * 0.5f;
				Vector2[] Ellipse( float a, float b ) => [.. Enumerable.Range( 0, steps * 2 ).Select( i => new Vector2( cx + MathF.Cos( MathF.PI * i / steps ) * a, cz + MathF.Sin( MathF.PI * i / steps ) * b ) )];
				return (Ellipse( rx, rz ), Ellipse( rx - w, rz - w ));
			}
			case WindowShape.HalfRound:
			{
				// A flat sill and a half ellipse over it.
				var rz = z1 - z0;
				var a = rx - w;
				var bz = rz - w;
				var start = MathF.Asin( Math.Clamp( w / MathF.Max( bz, 0.01f ), 0.0f, 0.99f ) );
				var outer = new List<Vector2>();
				var inner = new List<Vector2>();
				for ( var i = 0; i <= steps; i++ )
				{
					var t = MathF.PI * i / steps;
					outer.Add( new Vector2( cx + MathF.Cos( t ) * rx, z0 + MathF.Sin( t ) * rz ) );
					var u = start + (MathF.PI - start * 2.0f) * i / steps;
					inner.Add( new Vector2( cx + MathF.Cos( u ) * a, z0 + MathF.Sin( u ) * bz ) );
				}

				return ([.. outer], [.. inner]);
			}
			case WindowShape.Triangle:
			{
				Vector2[] outer = [new( x0, z0 ), new( x1, z0 ), new( cx, z1 )];
				return (outer, Inset( outer, w ));
			}
			default:
			{
				var c = MathF.Min( x1 - x0, z1 - z0 ) * 0.29f;
				Vector2[] outer = [new( x0 + c, z0 ), new( x1 - c, z0 ), new( x1, z0 + c ), new( x1, z1 - c ), new( x1 - c, z1 ), new( x0 + c, z1 ), new( x0, z1 - c ), new( x0, z0 + c )];
				return (outer, Inset( outer, w ));
			}
		}
	}

	/// <summary>
	/// The highest point of a closed outline straight above x.
	/// </summary>
	static float Under( IReadOnlyList<Vector2> outline, float x )
	{
		var best = float.MinValue;
		for ( var i = 0; i < outline.Count; i++ )
		{
			var a = outline[i];
			var b = outline[(i + 1) % outline.Count];
			if ( (x < a.x || x > b.x) && (x < b.x || x > a.x) || MathF.Abs( b.x - a.x ) < 0.0001f ) continue;
			best = MathF.Max( best, a.y + (b.y - a.y) * (x - a.x) / (b.x - a.x) );
		}

		return best;
	}

	/// <summary>
	/// Sheet metal laid on the surface around a hole, thin at its outer edge and standing up
	/// against the frame. The surface lies at y = surface, the sheet on its front.
	/// </summary>
	public static void Flashing( MeshKit kit, float x0, float x1, float z0, float z1, float surface, float width, float apron, float upstand )
	{
		if ( width <= 0.05f ) return;

		var hole = Rect( x0, x1, z0, z1 );
		var outer = new Vector2[] { new( x0 - width, z0 - width - apron ), new( x1 + width, z0 - width - apron ), new( x1 + width, z1 + width ), new( x0 - width, z1 + width ) };

		Ring( kit, outer, hole, 1.0f, [new( 0, 0 ), new( 0, -0.08f ), new( 0.8f, -0.12f ), new( 1, -MathF.Max( upstand, 0.15f ) ), new( 1, 0 )], surface );
	}

	/// <summary>
	/// The lining of a hole behind a window: four boards with mitred corners, from y0 back to y1.
	/// </summary>
	public static void Lining( MeshKit kit, float x0, float x1, float z0, float z1, float y0, float y1, float thickness )
	{
		if ( y1 - y0 < 0.05f || thickness <= 0.02f ) return;

		var hole = Rect( x0, x1, z0, z1 );

		Ring( kit, hole, Inset( hole, thickness ), thickness, [new( 0, y0 ), new( thickness, y0 ), new( thickness, y1 ), new( 0, y1 )], 0.0f );
	}

	/// <summary>
	/// A sill under an opening: its top falls to the front, it reaches past the opening on both
	/// sides by <paramref name="horn"/>. Its back lies at y = back, its top edge there at z.
	/// </summary>
	public static void Sill( MeshKit kit, float x0, float x1, float z, float back, float projection, float thickness, float horn )
	{
		if ( projection <= 0.05f || thickness <= 0.05f ) return;

		var fall = MathF.Min( thickness * 0.3f, projection * 0.2f );
		var nose = MathF.Min( thickness * 0.35f, 0.4f );
		Vector3[] End( float x ) =>
		[
			new( x, back, z ), new( x, back - projection, z - fall ), new( x, back - projection, z - thickness ),
			new( x, back - projection + nose, z - thickness ), new( x, back - projection + nose, z - thickness + nose * 0.5f ), new( x, back, z - thickness + nose * 0.5f )
		];

		Loft( kit, End( x0 - horn ), End( x1 + horn ) );
	}
}