editor/AdvancedMappingTools/Core/WindowUnit.cs

Editor-side geometry builder for Advanced Mapping Tools. Defines the window and door spec types (WindowType, WindowUnitSpec, DoorStyle, DoorSpec) and a static WindowUnit class that builds procedural frames, sashes, louvres, glass blocks, folding panels, hinges, handles, and several door kinds into a MeshKit.

namespace AdvancedMappingTools;

/// <summary>
/// How a window opens, or what it is made of.
/// </summary>
public enum WindowType
{
	[Title( "Fixed / Picture" )] Fixed,
	[Title( "Casement (Side Hinged)" )] Casement,
	[Title( "Double Casement (French)" )] DoubleCasement,
	[Title( "Single Hung" )] SingleHung,
	[Title( "Double Hung" )] DoubleHung,
	[Title( "Awning (Top Hinged)" )] Awning,
	[Title( "Hopper (Bottom Hinged)" )] Hopper,
	[Title( "Sliding" )] Sliding,
	[Title( "Jalousie (Louvres)" )] Jalousie,
	[Title( "Glass Block" )] GlassBlock,
	[Title( "Accordion (Folding)" )] Accordion,
	[Title( "Palladian" )] Palladian,
	[Title( "Pivot, Horizontal Axis" )] PivotHorizontal,
	[Title( "Pivot, Vertical Axis" )] PivotVertical
}

/// <summary>
/// One window unit: its type, how far it is open, and the look of its frames.
/// </summary>
public sealed class WindowUnitSpec
{
	public WindowType Type { get; set; } = WindowType.Fixed;
	/// <summary>0 closed, 1 fully open.</summary>
	public float Open { get; set; }
	/// <summary>Look of the frame and the sashes: corner, profile, detail, glazing, panes, materials.</summary>
	public WindowSpec Look { get; set; } = new();
	/// <summary>Handles, hinges and locks.</summary>
	public bool Hardware { get; set; } = true;
	public Material HardwareMaterial { get; set; }
	/// <summary>A fixed light above the window, as a share of its height; 0 for none.</summary>
	public float Transom { get; set; }
	/// <summary>A thin storm sash in front of the window.</summary>
	public bool Storm { get; set; }
	/// <summary>Louvres of a jalousie; 0 picks a number from the height.</summary>
	public int Slats { get; set; }
	/// <summary>Panels of a folding window.</summary>
	public int Panels { get; set; } = 4;
	/// <summary>Size of one glass block.</summary>
	public float BlockSize { get; set; } = 8;
}

/// <summary>
/// How a door leaf is made.
/// </summary>
public enum DoorStyle
{
	[Title( "Panelled" )] Panelled,
	[Title( "Plain" )] Plain,
	[Title( "Glazed" )] Glazed
}

/// <summary>
/// A door in a profiled frame: one or two leaves opening inward, with handles.
/// </summary>
public sealed class DoorSpec
{
	public WindowSpec Look { get; set; } = new();
	public DoorStyle Style { get; set; } = DoorStyle.Panelled;
	public int Leaves { get; set; } = 1;
	/// <summary>0 closed, 1 open about 85 degrees, inward.</summary>
	public float Open { get; set; }
	public bool Hardware { get; set; } = true;
	public Material LeafMaterial { get; set; }
	public Material HardwareMaterial { get; set; }
	/// <summary>Hinged leaves, a sliding leaf on a rail inside, a sectional garage door, or barn leaves on a rail outside.</summary>
	public DoorKind Kind { get; set; } = DoorKind.Hinged;
	/// <summary>How far the wall faces lie inside the frame's front and back: rails are fixed to the wall there.</summary>
	public float Mount { get; set; }
	/// <summary>
	/// When set, hinged leaves are not built into the kit but handed out here, each in the frame of
	/// its hinge with the way it turns open, so they can become objects that open in the game.
	/// </summary>
	public List<(Transform Hinge, PolygonMesh Mesh, float Turn)> SeparateLeaves { get; set; }
}

/// <summary>
/// Whole windows from <see cref="WindowKit"/>: a fixed frame with sashes that really open,
/// louvres, glass blocks, folding panels, transom lights, storm sashes and Palladian windows.
/// Same frame as WindowKit: the hole x0..x1, z0..z1, the front at y = front, depth behind it.
/// Every moving piece is placed so that it cuts into nothing at any opening.
/// </summary>
public static class WindowUnit
{
	public static WindowSpec Copy( WindowSpec s ) => new()
	{
		Width = s.Width, Depth = s.Depth, Edge = s.Edge, CornerSize = s.CornerSize, Profile = s.Profile, Corner = s.Corner,
		Detail = s.Detail, Glass = s.Glass, Glazing = s.Glazing, GlassInset = s.GlassInset, Columns = s.Columns, Rows = s.Rows,
		BarWidth = s.BarWidth, FrameMaterial = s.FrameMaterial, GlassMaterial = s.GlassMaterial, DetailMaterial = s.DetailMaterial,
		Arch = s.Arch, ArchRise = s.ArchRise, ArchSegments = s.ArchSegments, Shape = s.Shape
	};

	/// <summary>
	/// The outline of the hole a unit needs, counter clockwise seen from the front.
	/// </summary>
	public static Vector2[] Outline( float x0, float x1, float z0, float z1, WindowUnitSpec u )
	{
		var s = u.Look;
		if ( u.Type == WindowType.Palladian )
		{
			var (a, b, spring) = Palladian( x0, x1, z0, z1 );
			var curve = MeshKit.ArchCurve( a, b, spring, z1 - spring, ArchStyle.Round, s.ArchSegments );
			return [new( x0, z0 ), new( x1, z0 ), new( x1, spring ), .. curve.Reverse(), new( x0, spring )];
		}

		if ( s.Shape == WindowShape.Round )
		{
			var steps = Math.Clamp( s.ArchSegments, 6, 32 ) * 4;
			return [.. Enumerable.Range( 0, steps ).Select( i => new Vector2( (x0 + x1) * 0.5f + MathF.Cos( MathF.PI * 2 * i / steps ) * (x1 - x0) * 0.5f, (z0 + z1) * 0.5f + MathF.Sin( MathF.PI * 2 * i / steps ) * (z1 - z0) * 0.5f ) )];
		}

		if ( s.Shape == WindowShape.HalfRound )
		{
			var steps = Math.Clamp( s.ArchSegments, 6, 32 ) * 2;
			return [.. Enumerable.Range( 0, steps + 1 ).Select( i => new Vector2( (x0 + x1) * 0.5f + MathF.Cos( MathF.PI * i / steps ) * (x1 - x0) * 0.5f, z0 + MathF.Sin( MathF.PI * i / steps ) * (z1 - z0) ) )];
		}

		if ( s.Shape == WindowShape.Triangle )
			return [new( x0, z0 ), new( x1, z0 ), new( (x0 + x1) * 0.5f, z1 )];

		if ( s.Shape == WindowShape.Octagon )
		{
			var c = MathF.Min( x1 - x0, z1 - z0 ) * 0.29f;
			return [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 )];
		}

		var rise = s.Arch == ArchStyle.Flat ? 0.0f : s.ArchRise > 0.0f ? s.ArchRise : MeshKit.ArchRise( s.Arch, x1 - x0 );
		if ( rise > 0.5f && rise < z1 - z0 - 1.0f )
			return [new( x0, z0 ), new( x1, z0 ), .. MeshKit.ArchCurve( x0, x1, z1 - rise, rise, s.Arch, s.ArchSegments ).Reverse()];

		return WindowKit.Rect( x0, x1, z0, z1 );
	}

	static (float A, float B, float Spring) Palladian( float x0, float x1, float z0, float z1 )
	{
		var width = x1 - x0;
		var a = x0 + width * 0.25f;
		var b = x1 - width * 0.25f;
		var spring = MathF.Max( z1 - (b - a) * 0.5f, z0 + (z1 - z0) * 0.35f );
		return (a, b, spring);
	}

	public static void Build( MeshKit kit, float x0, float x1, float z0, float z1, float front, WindowUnitSpec u )
	{
		if ( x1 - x0 < 2.0f || z1 - z0 < 2.0f ) return;

		var look = u.Look;
		var material = kit.Material;
		kit.BeginAssembly();

		// A storm sash: outside, clear of hinges and pegs; inside where the sashes open outward;
		// none where a pivot sash turns through both sides.
		var outward = u.Type is WindowType.Casement or WindowType.DoubleCasement or WindowType.Awning or WindowType.Accordion;
		if ( u.Storm && u.Type is not (WindowType.PivotHorizontal or WindowType.PivotVertical) )
		{
			var storm = Copy( look );
			storm.Width = MathF.Min( 1.2f, look.Width );
			storm.Depth = 1.0f;
			storm.Profile = FrameProfile.Aluminium;
			storm.Corner = FrameCorner.Mitred;
			storm.Glazing = Glazing.Single;
			storm.Columns = 1;
			storm.Rows = storm.Shape == WindowShape.Rectangle && storm.Arch == ArchStyle.Flat && (z1 - z0) > (x1 - x0) * 1.2f ? 2 : 1;
			storm.Detail = WindowDetail.Standard;
			WindowKit.Build( kit, x0, x1, z0, z1, outward ? front + MathF.Max( look.Depth, 3.8f ) + 0.3f : front - 2.6f, storm );
		}

		var shaped = look.Shape != WindowShape.Rectangle || look.Arch != ArchStyle.Flat;
		var type = shaped && u.Type != WindowType.Palladian ? WindowType.Fixed : u.Type;

		if ( type == WindowType.Palladian )
		{
			var (a, b, spring) = Palladian( x0, x1, z0, z1 );
			var side = Copy( look );
			side.Shape = WindowShape.Rectangle;
			side.Arch = ArchStyle.Flat;
			var centre = Copy( side );
			centre.Arch = ArchStyle.Round;
			centre.ArchRise = z1 - spring;
			WindowKit.Build( kit, x0, a, z0, spring, front, side );
			WindowKit.Build( kit, a, b, z0, z1, front, centre );
			WindowKit.Build( kit, b, x1, z0, spring, front, side );
		}
		else if ( u.Transom > 0.05f && type != WindowType.Fixed )
		{
			// A fixed light over the opening part, both in frames that meet on the transom.
			var split = z1 - (z1 - z0) * Math.Clamp( u.Transom, 0.1f, 0.6f );
			var light = Copy( look );
			light.Columns = Math.Max( look.Columns, 1 );
			light.Rows = 1;
			WindowKit.Build( kit, x0, x1, split, z1, front, light );
			Unit( kit, x0, x1, z0, split, front, u, type );
		}
		else
		{
			Unit( kit, x0, x1, z0, z1, front, u, type );
		}

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

	/// <summary>One window of the given type in the hole.</summary>
	static void Unit( MeshKit kit, float x0, float x1, float z0, float z1, float front, WindowUnitSpec u, WindowType type )
	{
		var look = u.Look;
		if ( type == WindowType.Fixed )
		{
			WindowKit.Build( kit, x0, x1, z0, z1, front, look );
			return;
		}

		// The box frame: profiled like the look, without glass; the sashes or the infill go inside.
		var box = Copy( look );
		box.Glass = false;
		box.Columns = 1;
		box.Rows = 1;
		// The sashes fill a square opening: corners that round it off or reach into it become plain
		// joints in the frame around opening sashes; the sashes keep the look.
		box.Corner = look.Corner switch
		{
			FrameCorner.Rounded or FrameCorner.Chamfered => FrameCorner.Mitred,
			FrameCorner.Overlapping or FrameCorner.Decorative => FrameCorner.Butt,
			_ => look.Corner
		};
		var frame = WindowKit.Build( kit, x0, x1, z0, z1, front, box );
		var w = frame.Width;
		var d = frame.Depth;
		float ix0 = x0 + w, ix1 = x1 - w, iz0 = z0 + w, iz1 = z1 - w;
		if ( ix1 - ix0 < 2.0f || iz1 - iz0 < 2.0f ) return;

		var open = Math.Clamp( u.Open, 0.0f, 1.0f );
		var sash = Copy( look );
		sash.Width = MathF.Max( MathF.Min( w * 0.8f, MathF.Min( ix1 - ix0, iz1 - iz0 ) * 0.2f ), 0.4f );
		sash.Shape = WindowShape.Rectangle;
		sash.Arch = ArchStyle.Flat;
		sash.Corner = WindowKit.IsSwept( look.Corner ) ? look.Corner : FrameCorner.Mitred;
		var hardware = u.HardwareMaterial ?? look.DetailMaterial ?? look.FrameMaterial ?? kit.Material;
		var origin = kit.Frame;

		switch ( type )
		{
			case WindowType.Casement:
			case WindowType.DoubleCasement:
			{
				var sd = MathF.Min( d * 0.6f, 2.5f );
				sash.Depth = sd;
				var angle = open * 85.0f;
				var leaves = type == WindowType.DoubleCasement ? 2 : 1;
				var span = (ix1 - ix0) / leaves;
				// A leaf turning about its outer front edge sweeps a little past its width on the far side.
				var gap = sd * sd / (2.0f * span) + 0.05f;
				for ( var leaf = 0; leaf < leaves; leaf++ )
				{
					var left = leaf == 0;
					var pivot = new Vector3( left ? ix0 : ix1, front, 0 );
					kit.Frame = origin.ToWorld( new Transform( pivot, Rotation.FromYaw( left ? -angle : angle ) ) );
					var l0 = left ? 0.0f : -(span - gap);
					var l1 = left ? span - gap : 0.0f;
					WindowKit.Build( kit, l0, l1, iz0, iz1, 0, sash );
					if ( u.Hardware )
					{
						kit.Material = hardware;
						var x = left ? l1 - sash.Width * 0.5f : l0 + sash.Width * 0.5f;
						Handle( kit, x, (iz0 + iz1) * 0.5f, sd, left ? 1 : -1 );
					}

					kit.Frame = origin;
					if ( u.Hardware )
					{
						kit.Material = hardware;
						Hinges( kit, left ? ix0 - 0.5f : ix1, left ? ix0 : ix1 + 0.5f, iz0, iz1, front );
					}
				}

				break;
			}
			case WindowType.Awning:
			case WindowType.Hopper:
			{
				var sd = MathF.Min( d * 0.6f, 2.5f );
				sash.Depth = sd;
				var height = iz1 - iz0;
				var gap = sd * sd / (2.0f * height) + 0.05f;
				var angle = open * (type == WindowType.Awning ? 60.0f : 35.0f) * MathF.PI / 180.0f;
				var up = new Vector3( 0, MathF.Sin( angle ), MathF.Cos( angle ) );
				if ( type == WindowType.Awning )
				{
					// Hinged at the head, the foot swings out.
					kit.Frame = origin.ToWorld( new Transform( new Vector3( 0, front, iz1 ), Rotation.LookAt( Vector3.Forward, up ) ) );
					WindowKit.Build( kit, ix0, ix1, -(height - gap), 0, 0, sash );
					if ( u.Hardware )
					{
						kit.Material = hardware;
						Handle( kit, (ix0 + ix1) * 0.5f, -(height - gap) + sash.Width * 0.5f, sd, 0 );
					}
				}
				else
				{
					// Hinged at the sill, the head tips in.
					kit.Frame = origin.ToWorld( new Transform( new Vector3( 0, front + sd, iz0 ), Rotation.LookAt( Vector3.Forward, up ) ) );
					WindowKit.Build( kit, ix0, ix1, 0, height - gap, -sd, sash );
					if ( u.Hardware )
					{
						kit.Material = hardware;
						Handle( kit, (ix0 + ix1) * 0.5f, height - gap - sash.Width * 0.5f, 0, 0 );
					}
				}

				kit.Frame = origin;
				break;
			}
			case WindowType.SingleHung:
			case WindowType.DoubleHung:
			case WindowType.Sliding:
			{
				// Two sashes in two tracks, overlapping at the meeting rail.
				var sd = MathF.Min( (d - 0.1f) * 0.5f, 2.5f );
				if ( sd < 0.3f ) { WindowKit.Build( kit, ix0, ix1, iz0, iz1, front, look ); break; }
				sash.Depth = sd;
				var outer = front + (d - sd * 2.0f - 0.05f) * 0.5f;
				var inner = outer + sd + 0.05f;
				var sliding = type == WindowType.Sliding;
				var length = sliding ? ix1 - ix0 : iz1 - iz0;
				var lap = sash.Width;
				var piece = length * 0.5f + lap * 0.5f;
				var travel = (length - piece) * open;
				var fixedMove = type == WindowType.DoubleHung ? travel * 0.5f : 0.0f;
				var moving = type == WindowType.DoubleHung ? travel * 0.5f : travel;
				if ( sliding )
				{
					WindowKit.Build( kit, ix0, ix0 + piece, iz0, iz1, outer, sash );
					WindowKit.Build( kit, ix1 - piece - moving, ix1 - moving, iz0, iz1, inner, sash );
					if ( u.Hardware )
					{
						kit.Material = hardware;
						Handle( kit, ix1 - piece - moving + sash.Width * 0.5f, (iz0 + iz1) * 0.5f, inner + sd, 1 );
					}
				}
				else
				{
					// Upper sash in the outer track, lower sash in the inner one.
					WindowKit.Build( kit, ix0, ix1, iz1 - piece - fixedMove, iz1 - fixedMove, outer, sash );
					WindowKit.Build( kit, ix0, ix1, iz0 + moving, iz0 + piece + moving, inner, sash );
					if ( u.Hardware )
					{
						kit.Material = hardware;
						var rail = iz0 + piece + moving - sash.Width * 0.5f;
						kit.BeginPart();
						kit.Box( new Vector3( (ix0 + ix1) * 0.5f - 1.0f, inner + sd, rail - 0.4f ), new Vector3( (ix0 + ix1) * 0.5f + 1.0f, inner + sd + 0.4f, rail + 0.4f ) );
					}
				}

				break;
			}
			case WindowType.PivotHorizontal:
			case WindowType.PivotVertical:
			{
				// One sash turning about an axis through its middle: half goes out, half comes in.
				var sd = MathF.Min( d * 0.6f, 2.5f );
				sash.Depth = sd;
				var level = type == WindowType.PivotHorizontal;
				var half = (level ? iz1 - iz0 : ix1 - ix0) * 0.5f;
				// The corners sweep a little past the half length.
				var gap = MathF.Sqrt( half * half + sd * sd * 0.25f ) - half + 0.05f;
				var angle = open * 80.0f;
				var middle = new Vector3( (ix0 + ix1) * 0.5f, front + d * 0.5f, (iz0 + iz1) * 0.5f );
				var turn = level
					? Rotation.LookAt( Vector3.Forward, new Vector3( 0, MathF.Sin( angle * MathF.PI / 180.0f ), MathF.Cos( angle * MathF.PI / 180.0f ) ) )
					: Rotation.FromYaw( angle );
				kit.Frame = origin.ToWorld( new Transform( middle, turn ) );
				if ( level )
					WindowKit.Build( kit, ix0 - middle.x, ix1 - middle.x, -(half - gap), half - gap, -sd * 0.5f, sash );
				else
					WindowKit.Build( kit, -(half - gap), half - gap, iz0 - middle.z, iz1 - middle.z, -sd * 0.5f, sash );
				if ( u.Hardware )
				{
					kit.Material = hardware;
					if ( level )
						Handle( kit, 0, -(half - gap) + sash.Width * 0.5f, sd * 0.5f, 0 );
					else
						Handle( kit, half - gap - sash.Width * 0.5f, 0, sd * 0.5f, 1 );
				}

				kit.Frame = origin;
				break;
			}
			case WindowType.Jalousie:
				Jalousie( kit, ix0, ix1, iz0, iz1, front, d, u, open, hardware );
				break;
			case WindowType.GlassBlock:
				Blocks( kit, ix0, ix1, iz0, iz1, front, d, u );
				break;
			case WindowType.Accordion:
				Accordion( kit, ix0, ix1, iz0, iz1, front, d, u, open, sash, hardware );
				break;
		}

		kit.Frame = origin;
	}

	/// <summary>
	/// A door in the hole x0..x1, z0..z1: a profiled frame with a threshold, and leaves of the chosen
	/// kind. Hinged leaves open inward behind the frame's back face, sliding leaves run on a rail on the
	/// inside wall, barn leaves on a rail outside, a garage door rolls its sections up into the room.
	/// Nothing touches the frame at any opening.
	/// </summary>
	public static void BuildDoor( MeshKit kit, float x0, float x1, float z0, float z1, float front, DoorSpec door )
	{
		if ( x1 - x0 < 4.0f || z1 - z0 < 4.0f ) return;
		var look = door.Look;
		var material = kit.Material;
		kit.BeginAssembly();
		var box = Copy( look );
		box.Glass = false;
		box.Columns = 1;
		box.Rows = 1;
		box.Shape = WindowShape.Rectangle;
		box.Arch = ArchStyle.Flat;
		box.Corner = look.Corner switch
		{
			FrameCorner.Rounded or FrameCorner.Chamfered => FrameCorner.Mitred,
			FrameCorner.Overlapping or FrameCorner.Decorative => FrameCorner.Butt,
			_ => look.Corner
		};
		var frame = WindowKit.Build( kit, x0, x1, z0, z1, front, box );
		var w = frame.Width;
		var d = frame.Depth;
		float ix0 = x0 + w, ix1 = x1 - w, iz0 = z0 + w, iz1 = z1 - w;
		if ( ix1 - ix0 < 2.0f || iz1 - iz0 < 2.0f ) { kit.EndAssembly(); kit.Material = material; return; }

		var open = Math.Clamp( door.Open, 0.0f, 1.0f );
		var thick = MathF.Max( MathF.Min( 2.0f, d * 0.5f ), 0.3f );
		var hardware = door.HardwareMaterial ?? look.DetailMaterial ?? look.FrameMaterial ?? kit.Material;
		var leafMaterial = door.LeafMaterial ?? look.FrameMaterial ?? kit.Material;
		var back = front + d;
		var mount = Math.Clamp( door.Mount, 0.0f, d * 0.5f );
		var origin = kit.Frame;

		switch ( door.Kind )
		{
			case DoorKind.Sliding:
			case DoorKind.Barn:
			{
				// Leaves hang from a rail fixed to the wall above the opening and slide along it,
				// inside the room for a sliding door, outside for a barn door.
				var inside = door.Kind == DoorKind.Sliding;
				var leaves = Math.Clamp( door.Leaves, 1, 2 );
				var lap = 2.0f;
				var width = (ix1 - ix0) / leaves + lap;
				var face = inside ? back + 0.1f : front - 0.1f;
				var y0 = inside ? face : face - thick;
				var y1 = inside ? face + thick : face;
				var top = z1 + 1.0f;
				for ( var leaf = 0; leaf < leaves; leaf++ )
				{
					var left = leaf == 0;
					var travel = open * width;
					var a = left ? ix0 - lap - travel : (ix0 + ix1) * 0.5f + travel;
					var b = a + width;
					if ( leaves == 1 ) { a = ix0 - lap - travel; b = ix1 + lap - travel; }
					kit.Material = leafMaterial;
					Plank( kit, a, b, z0 + 0.5f, top, y0, y1, door.Style, inside ? 1.0f : -1.0f );
					if ( door.Hardware )
					{
						kit.Material = hardware;
						var x = left ? b - 3.0f : a + 3.0f;
						kit.BeginPart();
						var yh = inside ? y1 : y0 - 0.8f;
						kit.Box( new Vector3( x - 0.4f, yh, z0 + 34.0f ), new Vector3( x + 0.4f, yh + 0.8f, z0 + 46.0f ) );
					}
				}

				// The rail, on the wall over the opening, reaching as far as the leaves can go.
				var reach = (ix1 - ix0) / leaves + lap * 2.0f;
				var r0 = ix0 - lap - reach;
				var r1 = leaves == 1 ? ix1 + lap : ix1 + lap + reach;
				kit.Material = hardware;
				kit.BeginPart();
				if ( inside )
					kit.Box( new Vector3( r0, back - mount, top ), new Vector3( r1, y1 + 0.6f, top + 3.0f ) );
				else
					kit.Box( new Vector3( r0, y0 - 0.6f, top ), new Vector3( r1, front + mount, top + 3.0f ) );
				break;
			}
			case DoorKind.Garage:
			{
				// Sections run up the opening and turn back into the room above it.
				var height = iz1 - iz0;
				var count = Math.Clamp( (int)MathF.Round( height / 20.0f ), 3, 12 );
				var section = height / count;
				var offset = open * height;
				var middle = front + (d - thick) * 0.5f;
				var stacked = 0;
				kit.Material = leafMaterial;
				for ( var i = count - 1; i >= 0; i-- )
				{
					var bottom = iz0 + offset + section * i;
					kit.BeginPart();
					if ( bottom + section - 0.1f <= iz1 + 0.001f )
					{
						kit.Box( new Vector3( ix0, middle, bottom ), new Vector3( ix1, middle + thick, bottom + section - 0.1f ) );
						if ( door.Hardware && i == 0 )
						{
							kit.Material = hardware;
							kit.BeginPart();
							kit.Box( new Vector3( (ix0 + ix1) * 0.5f - 3.0f, middle - 0.8f, bottom + 3.0f ), new Vector3( (ix0 + ix1) * 0.5f + 3.0f, middle, bottom + 4.0f ) );
							kit.Material = leafMaterial;
						}
					}
					else
					{
						// Lying flat in the room under the ceiling, the first one up the furthest in.
						var y = back + 0.5f + stacked * section;
						kit.Box( new Vector3( ix0, y, z1 + 0.5f ), new Vector3( ix1, y + section - 0.1f, z1 + 0.5f + thick ) );
						stacked++;
					}
				}

				break;
			}
			default:
			{
				var leaves = Math.Clamp( door.Leaves, 1, 2 );
				var span = (ix1 - ix0) / leaves;
				var gap = thick * thick / (2.0f * span) + 0.05f;
				var angle = open * 85.0f;
				for ( var leaf = 0; leaf < leaves; leaf++ )
				{
					var left = leaf == 0;
					// Hinged on the inside edge of the jamb, the leaf flush with the frame's back face.
					var hinge = new Transform( new Vector3( left ? ix0 : ix1, back, 0 ), Rotation.FromYaw( left ? angle : -angle ) );
					var l0 = left ? 0.0f : -(span - gap);
					var l1 = left ? span - gap : 0.0f;
					var target = kit;
					if ( door.SeparateLeaves is not null )
						target = new MeshKit { Material = leafMaterial };
					else
						kit.Frame = origin.ToWorld( hinge );
					HingedLeaf( target, door, look, box.Corner, l0, l1, iz0, iz1, thick, leafMaterial, hardware, left );
					if ( door.SeparateLeaves is not null )
						door.SeparateLeaves.Add( (hinge, target.Finish(), left ? 1.0f : -1.0f) );
					kit.Frame = origin;
				}

				break;
			}
		}

		kit.Frame = origin;
		kit.EndAssembly();
		kit.Material = material;
	}

	/// <summary>One hinged leaf from l0 to l1 in the frame of its hinge, its back face at y = 0.</summary>
	static void HingedLeaf( MeshKit kit, DoorSpec door, WindowSpec look, FrameCorner corner, float l0, float l1, float iz0, float iz1, float thick, Material leafMaterial, Material hardware, bool left )
	{
		kit.Material = leafMaterial;
		if ( door.Style == DoorStyle.Plain )
		{
			kit.BeginPart();
			kit.Box( new Vector3( l0, -thick, iz0 ), new Vector3( l1, 0, iz1 ) );
		}
		else
		{
			var stile = MathF.Min( 4.0f, MathF.Min( l1 - l0, iz1 - iz0 ) * 0.2f );
			var leafSpec = Copy( look );
			leafSpec.Width = stile;
			leafSpec.Depth = thick;
			leafSpec.Shape = WindowShape.Rectangle;
			leafSpec.Arch = ArchStyle.Flat;
			leafSpec.Corner = corner;
			leafSpec.FrameMaterial = leafMaterial;
			leafSpec.Glass = door.Style == DoorStyle.Glazed;
			leafSpec.Columns = 1;
			leafSpec.Rows = door.Style == DoorStyle.Glazed ? 3 : 1;
			var made = WindowKit.Build( kit, l0, l1, iz0, iz1, -thick, leafSpec );
			if ( door.Style == DoorStyle.Panelled )
			{
				// The panel sits in the middle of the leaf, inside its stiles and rails.
				var sw = made.Width;
				kit.BeginPart();
				kit.Box( new Vector3( l0 + sw, -thick * 0.7f, iz0 + sw ), new Vector3( l1 - sw, -thick * 0.3f, iz1 - sw ) );
			}
		}

		if ( !door.Hardware ) return;
		kit.Material = hardware;
		var x = left ? l1 - 3.0f : l0 + 3.0f;
		var z = MathF.Min( iz0 + 40.0f, (iz0 + iz1) * 0.5f );
		Handle( kit, x, z, 0, left ? 1 : -1 );
		// The outside knob.
		kit.BeginPart();
		kit.Box( new Vector3( x - 0.4f, -thick - 0.6f, z - 0.4f ), new Vector3( x + 0.4f, -thick, z + 0.4f ) );
	}

	/// <summary>
	/// A sliding or barn leaf from a to b, z0 to z1, between y0 and y1: boards for a barn door
	/// with braces on its outer face (<paramref name="outward"/> -1 toward -Y), a framed panel otherwise.
	/// </summary>
	static void Plank( MeshKit kit, float a, float b, float z0, float z1, float y0, float y1, DoorStyle style, float outward )
	{
		kit.BeginPart();
		kit.Box( new Vector3( a, y0, z0 ), new Vector3( b, y1, z1 ) );
		if ( style == DoorStyle.Plain ) return;

		// Two rails and a diagonal brace standing on the face away from the wall.
		var face = outward > 0 ? y1 : y0;
		var f0 = outward > 0 ? face : face - 0.6f;
		var f1 = outward > 0 ? face + 0.6f : face;
		var rail = MathF.Min( 6.0f, (z1 - z0) * 0.08f );
		kit.BeginPart();
		kit.Box( new Vector3( a + 1, f0, z0 + 4 ), new Vector3( b - 1, f1, z0 + 4 + rail ) );
		kit.BeginPart();
		kit.Box( new Vector3( a + 1, f0, z1 - 4 - rail ), new Vector3( b - 1, f1, z1 - 4 ) );
		var lo = z0 + 4 + rail;
		var hi = z1 - 4 - rail;
		if ( hi - lo > rail * 2.0f )
		{
			var half = rail * 0.5f;
			WindowKit.Loft( kit,
				[new Vector3( a + 1, f0, lo ), new Vector3( a + 1 + rail, f0, lo ), new Vector3( b - 1, f0, hi - half * 0 ), new Vector3( b - 1 - rail, f0, hi )],
				[new Vector3( a + 1, f1, lo ), new Vector3( a + 1 + rail, f1, lo ), new Vector3( b - 1, f1, hi - half * 0 ), new Vector3( b - 1 - rail, f1, hi )] );
		}
	}

	/// <summary>
	/// A lever handle on the inside face (y = back from the sash front), at x, z. Lever points
	/// down; side +1 and -1 turn its rose toward the middle of the sash.
	/// </summary>
	static void Handle( MeshKit kit, float x, float z, float back, int side )
	{
		kit.BeginPart();
		kit.Box( new Vector3( x - 0.4f, back, z - 1.0f ), new Vector3( x + 0.4f, back + 0.35f, z + 1.0f ) );
		kit.BeginPart();
		kit.Box( new Vector3( x - 0.25f, back + 0.35f, z - 0.25f ), new Vector3( x + 0.25f, back + 0.9f, z + 0.25f ) );
		kit.BeginPart();
		// On a rail the lever lies level, on a stile it hangs down.
		if ( side == 0 )
			kit.Box( new Vector3( x - 2.5f, back + 0.9f, z - 0.25f ), new Vector3( x + 0.25f, back + 1.3f, z + 0.25f ) );
		else
			kit.Box( new Vector3( x - 0.25f, back + 0.9f, z - 2.5f ), new Vector3( x + 0.25f, back + 1.3f, z + 0.25f ) );
	}

	/// <summary>Two hinge knuckles on the front of the frame beside the opening edge.</summary>
	static void Hinges( MeshKit kit, float x0, float x1, float z0, float z1, float front )
	{
		foreach ( var z in new[] { z0 + (z1 - z0) * 0.15f, z1 - (z1 - z0) * 0.15f } )
		{
			kit.BeginPart();
			kit.Box( new Vector3( x0, front - 0.5f, z - 1.25f ), new Vector3( x1, front, z + 1.25f ) );
		}
	}

	/// <summary>
	/// Glass louvres turning about level axes through the middle of the frame depth. Neighbours
	/// stay apart at every angle; the top and bottom ones stay clear of head and sill.
	/// </summary>
	static void Jalousie( MeshKit kit, float x0, float x1, float z0, float z1, float front, float d, WindowUnitSpec u, float open, Material hardware )
	{
		var height = z1 - z0;
		var count = u.Slats > 0 ? Math.Clamp( u.Slats, 2, 40 ) : Math.Clamp( (int)MathF.Round( height / 4.0f ), 3, 40 );
		var tilt = (15.0f + open * 65.0f) * MathF.PI / 180.0f;
		const float thick = 0.25f;
		var pitch = height / count;
		var slat = MathF.Min( pitch * 1.25f, (d - 0.2f - thick * MathF.Cos( tilt )) / MathF.Max( MathF.Sin( tilt ), 0.05f ) );
		var reach = (slat * MathF.Cos( tilt ) + thick * MathF.Sin( tilt )) * 0.5f + 0.05f;
		if ( height - reach * 2.0f < 0.5f || slat < 0.5f ) return;
		var step = count > 1 ? (height - reach * 2.0f) / (count - 1) : 0.0f;
		if ( count > 1 && step * MathF.Sin( tilt ) < thick + 0.05f )
		{
			count = Math.Max( 2, (int)((height - reach * 2.0f) / ((thick + 0.05f) / MathF.Sin( tilt ))) + 1 );
			step = (height - reach * 2.0f) / (count - 1);
		}

		var y = front + d * 0.5f;
		var axis = new Vector3( 0, MathF.Sin( tilt ), MathF.Cos( tilt ) );
		var across = new Vector3( 0, MathF.Cos( tilt ), -MathF.Sin( tilt ) );
		kit.Material = u.Look.GlassMaterial ?? Material.Load( "materials/glass/glass_a.vmat" );
		for ( var i = 0; i < count; i++ )
		{
			var centre = new Vector3( 0, y, z0 + reach + step * i );
			Vector3 P( float x, float a, float b ) => centre.WithX( x ) + axis * a + across * b;
			WindowKit.Loft( kit, [P( x0, -slat * 0.5f, thick * 0.5f ), P( x0, slat * 0.5f, thick * 0.5f ), P( x0, slat * 0.5f, -thick * 0.5f ), P( x0, -slat * 0.5f, -thick * 0.5f )],
				[P( x1, -slat * 0.5f, thick * 0.5f ), P( x1, slat * 0.5f, thick * 0.5f ), P( x1, slat * 0.5f, -thick * 0.5f ), P( x1, -slat * 0.5f, -thick * 0.5f )] );
		}
	}

	/// <summary>Glass blocks in a grid with mortar joints between them.</summary>
	static void Blocks( MeshKit kit, float x0, float x1, float z0, float z1, float front, float d, WindowUnitSpec u )
	{
		var size = Math.Clamp( u.BlockSize, 3.0f, 24.0f );
		var columns = Math.Max( 1, (int)MathF.Round( (x1 - x0) / size ) );
		var rows = Math.Max( 1, (int)MathF.Round( (z1 - z0) / size ) );
		var joint = MathF.Min( 0.6f, MathF.Min( (x1 - x0) / columns, (z1 - z0) / rows ) * 0.15f );
		var cw = (x1 - x0 - joint * (columns - 1)) / columns;
		var ch = (z1 - z0 - joint * (rows - 1)) / rows;
		var y0 = front + MathF.Min( 0.3f, d * 0.1f );
		var y1 = front + d - MathF.Min( 0.3f, d * 0.1f );
		var glass = u.Look.GlassMaterial ?? Material.Load( "materials/glass/glass_a.vmat" );
		var mortar = u.Look.DetailMaterial ?? u.Look.FrameMaterial ?? kit.Material;
		for ( var c = 0; c < columns; c++ )
			for ( var r = 0; r < rows; r++ )
			{
				var bx = x0 + c * (cw + joint);
				var bz = z0 + r * (ch + joint);
				kit.Material = glass;
				kit.BeginPart();
				kit.Box( new Vector3( bx, y0, bz ), new Vector3( bx + cw, y1, bz + ch ) );
			}

		kit.Material = mortar;
		for ( var c = 1; c < columns; c++ )
		{
			var x = x0 + c * (cw + joint) - joint;
			kit.BeginPart();
			kit.Box( new Vector3( x, y0, z0 ), new Vector3( x + joint, y1, z1 ) );
		}

		for ( var r = 1; r < rows; r++ )
			for ( var c = 0; c < columns; c++ )
			{
				var z = z0 + r * (ch + joint) - joint;
				var bx = x0 + c * (cw + joint);
				kit.BeginPart();
				kit.Box( new Vector3( bx, y0, z ), new Vector3( bx + cw, y1, z + joint ) );
			}
	}

	/// <summary>
	/// Glazed panels folding like an accordion toward the left jamb. The gap at every fold grows
	/// with the angle so neighbouring panels never meet.
	/// </summary>
	static void Accordion( MeshKit kit, float x0, float x1, float z0, float z1, float front, float d, WindowUnitSpec u, float open, WindowSpec sash, Material hardware )
	{
		var count = Math.Clamp( u.Panels, 2, 12 );
		var thick = MathF.Min( d * 0.5f, 2.0f );
		var fold = open * 60.0f * MathF.PI / 180.0f;
		var gap = 2.0f * thick * MathF.Sin( fold ) + 0.1f;
		// The folds need room at a slant: the chain of panels must still fit between the jambs.
		var panel = MathF.Min( (x1 - x0 - 0.1f * (count - 1)) / count, (x1 - x0 - gap * (count - 1) - 0.05f) / (count * MathF.Cos( fold )) );
		if ( panel < 2.0f ) return;
		var p = sash;
		p.Depth = thick;
		p.Width = MathF.Max( MathF.Min( p.Width, panel * 0.2f ), 0.3f );
		var origin = kit.Frame;
		var at = new Vector2( x0, front + (d - thick) * 0.5f );
		for ( var i = 0; i < count; i++ )
		{
			var dir = new Vector2( MathF.Cos( fold ), i % 2 == 0 ? -MathF.Sin( fold ) : MathF.Sin( fold ) );
			var yaw = MathF.Atan2( dir.y, dir.x ) * 180.0f / MathF.PI;
			kit.Frame = origin.ToWorld( new Transform( new Vector3( at.x, at.y, 0 ), Rotation.FromYaw( yaw ) ) );
			WindowKit.Build( kit, 0, panel, z0, z1, 0, p );
			at += dir * panel + new Vector2( gap, 0 );
		}

		kit.Frame = origin;
	}
}