editor/AdvancedMappingTools/Geometry/RoofEditor.cs

An editor tool that draws roof geometry. The enum lists about 24 roof shapes (gable, hip, mansard, sawtooth, dome and so on). RectEditor is the base of a three click rectangle tool, and RoofEditor builds the roof mesh with MeshKit: slopes, edge trim, ridge cap, gutters, tile rows and optional dormers that can dock onto finished wall tops. It also has a Custom profile mode.

namespace AdvancedMappingTools;
public enum RoofShape
{
	Flat,
	Shed,
	Gable,
	Hip,
	Gambrel,
	Mansard,
	Saltbox,
	Butterfly,
	Barrel,
	[Title( "Cross Gable" )] CrossGable,
	Pyramid,
	[Title( "Jerkinhead (Clipped Gable)" )] Jerkinhead,
	[Title( "Dutch Gable" )] DutchGable,
	Monitor,
	Sawtooth,
	[Title( "M Shape (Double Gable)" )] MShape,
	[Title( "Bellcast (Flared Gable)" )] Bellcast,
	Clerestory,
	Conical,
	Dome,
	[Title( "Gothic (Pointed Barrel)" )] Gothic,
	[Title( "Gull Wing" )] GullWing,
	[Title( "Bonnet (Kicked Hip)" )] Bonnet,
	[Title( "Custom Profile" )] Custom
}

/// <summary>
/// A rectangle set by three clicks: one edge, then the depth.
/// </summary>
public abstract class RectEditor( PrimitiveTool tool ) : BuilderEditor( tool )
{
	protected override int MinPoints => 3;
	protected override int MaxPoints => 3;

	protected override IEnumerable<string> Hints => ["Points 1 and 2: first edge", "Point 3: depth"];

	Vector3 Edge => (Points[1].Position - Points[0].Position).WithZ( 0.0f );
	Vector3 Across => new( -Edge.Normal.y, Edge.Normal.x, 0.0f );
	float SignedDepth => (Points[2].Position - Points[0].Position).Dot( Across );

	protected float RectLength => Edge.Length;
	protected float RectDepth => MathF.Abs( SignedDepth );

	/// <summary>
	/// Frame on the corner of the rectangle, +X along the first edge and +Y across it.
	/// </summary>
	protected Transform RectFrame => new(
		Points[0].Position + Across * MathF.Min( SignedDepth, 0.0f ),
		Rotation.LookAt( Edge.Normal, Vector3.Up ) );

	protected override bool IsShapeValid => Edge.Length > 1.0f && RectDepth > 1.0f;
}

/// <summary>
/// A roof over a rectangle. The first two points set one edge and its ridge direction, the third the depth.
/// </summary>
[ToolCategory( "Roofs" )]
[Title( "Roof" ), Icon( "roofing" )]
public sealed partial class RoofEditor( PrimitiveTool tool ) : RectEditor( tool )
{
	[Property, Group( "Roof" ), WideMode]
	public RoofShape Shape { get; set; } = RoofShape.Gable;

	[Property, Group( "Roof" ), Title( "Ridge Height" ), Range( 0, 2048, slider: false ), WideMode]
	public float RidgeHeight { get; set; } = 48.0f;

	[Property, Group( "Roof" ), Title( "Edge Thickness" ), Range( 0.25f, 128, slider: false ), WideMode]
	public float EdgeThickness { get; set; } = 4.0f;

	[Property, Group( "Roof" ), Range( 0, 256, slider: false ), WideMode]
	public float Overhang { get; set; } = 8.0f;

	[Property, Group( "Roof" ), Range( -4096, 4096, slider: false ), WideMode, Description( "Raises the roof above the drawn points." )]
	public float Elevation { get; set; } = 0.0f;

	[Property, ShowIf( nameof( Shape ), RoofShape.Custom ), Group( "Custom Profile" ), Title( "Profile" ), WideMode, Description( "For the Custom Profile shape: points across the roof from the front eave (X 0) to the back eave (X 1), Y from the eave (0) to the ridge height (1)." )]
	public List<Vector2> CustomProfile { get; set; } = [new( 0.0f, 0.0f ), new( 0.35f, 0.9f ), new( 0.5f, 1.0f ), new( 0.65f, 0.9f ), new( 1.0f, 0.0f )];

	[Property, Group( "Details" ), Title( "Ridge Cap" ), WideMode, Description( "A bar along the top of a gable roof." )]
	public bool RidgeCap { get; set; } = false;

	[Property, Group( "Details" ), Title( "Tile Rows" ), Range( 0, 64, slider: false ), WideMode, Description( "Raised rows across the slopes of a gable roof, in the trim material." )]
	public int TileRows { get; set; } = 0;

	[Property, Group( "Details" ), WideMode, Description( "A gutter along both eaves." )]
	public bool Gutters { get; set; } = false;

	[Property, Advanced, Group( "Roof" ), Title( "Wall Margin" ), Range( 0, 256, slider: false ), WideMode, Description( "Extra width all round. Set to half the wall when the corners snap onto walls, so the roof covers the walls before the overhang starts." )]
	public float WallMargin { get; set; } = 0.0f;

	protected override bool Connects => true;

	protected override IEnumerable<string> Hints => [.. base.Hints, "Click on a wall corner: the roof sits on the walls"];

	/// <summary>
	/// A point near the top corner of a finished wall lands on it, so the roof sits on the walls.
	/// </summary>
	protected override Vector3 Dock( Vector3 point )
	{
		var docked = base.Dock( point );

		if ( !ConnectFinished || SceneEditorSession.Active is null ) return docked;

		var corner = WallTops.Nearest( SceneEditorSession.Active.Scene, docked, DockRadius + CornerReach );

		if ( Points.Count == 0 ) WallMargin = corner?.Half ?? 0.0f;

		if ( corner is not { } found ) return docked;

		WallMargin = MathF.Max( WallMargin, found.Half );
		DrawLabel( found.Top, "On the wall", Color.Green );

		return found.Top;
	}

	const float CornerReach = 16.0f;

	protected override Vector3? UnderCursor( Vector3 point ) => WallCornerUnderCursor( point, CornerReach ) ?? base.UnderCursor( point );

	[Property, Group( "Dormers" ), Range( 0, 16, slider: false ), WideMode, Description( "Small gabled windows on the front slope of a gable roof." )]
	public int Dormers { get; set; } = 0;

	[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Width" ), Range( 8, 256, slider: false ), WideMode]
	public float DormerWidth { get; set; } = 32.0f;

	[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Height" ), Range( 8, 256, slider: false ), WideMode]
	public float DormerHeight { get; set; } = 24.0f;

	[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Roof" ), WideMode, Description( "Gable, shed, flat, hipped, round barrel or segmental arch." )]
	public DormerShape DormerRoof { get; set; } = DormerShape.Gable;

	[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Roof Rise" ), Range( 2, 128, slider: false ), WideMode]
	public float DormerRoofRise { get; set; } = 16.0f;

	[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Overhang" ), Range( 0, 32, slider: false ), WideMode]
	public float DormerOverhang { get; set; } = 3.0f;

	[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormers On Both Sides" ), WideMode]
	public bool DormersBothSides { get; set; }

	[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Window" ), WideMode]
	public bool DormerWindow { get; set; } = true;

	[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Window Width" ), Range( 6, 256, slider: false ), WideMode]
	public float DormerWindowWidth { get; set; } = 20.0f;

	[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Window Height" ), Range( 6, 256, slider: false ), WideMode]
	public float DormerWindowHeight { get; set; } = 16.0f;

	[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Window Corners" ), WideMode]
	public FrameCorner DormerWindowCorner { get; set; } = FrameCorner.Mitred;

	[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Window Detail" ), WideMode]
	public WindowDetail DormerWindowDetail { get; set; } = WindowDetail.Standard;

	[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Flashing" ), WideMode]
	public bool DormerFlashing { get; set; } = true;

	/// <summary>Where the dormers stand on the roof, in its length and depth. Tile rows stop there.</summary>
	readonly List<(float X0, float X1, float Y0, float Y1)> _dormerFootprints = [];

	/// <summary>
	/// One row of tiles along the roof. It stops at roof windows and cut dormers and goes on behind them.
	/// </summary>
	void TileRow( MeshKit kit, float x0, float x1, float y, float z )
	{
		var from = x0;

		foreach ( var hole in _roofOpenings.Concat( _dormerFootprints ).Where( h => y > h.Y0 - 1.0f && y < h.Y1 + 1.0f && h.X1 > x0 && h.X0 < x1 ).OrderBy( h => h.X0 ) )
		{
			if ( hole.X0 - from > 1.0f )
				kit.Beam( new Vector3( from, y, z ), new Vector3( hole.X0, y, z ), 2.0f, 1.0f );

			from = MathF.Max( from, hole.X1 );
		}

		if ( x1 - from > 1.0f )
			kit.Beam( new Vector3( from, y, z ), new Vector3( x1, y, z ), 2.0f, 1.0f );
	}

	/// <summary>
	/// Dormers on the front slope of a gable roof, or on both: each stands on the slope with its
	/// own roof ending in the main roof, a window in a real reveal and flashing at its foot.
	/// </summary>
	void BuildDormers( MeshKit kit, Transform frame, float y0, float y1, float middle )
	{
		_dormerFootprints.Clear();
		if ( !EnableDormers || Dormers <= 0 || (Hollow && CutDormerOpenings) )
			return;

		var slot = RectLength / Dormers;
		var run = MathF.Max( middle - y0, 1.0f );
		var tan = RidgeHeight / run;
		var pitch = MathF.Atan( tan ) * 180.0f / MathF.PI;
		var floor = EdgeThickness + RidgeHeight * 0.4f;
		var glass = Material.Load( "materials/glass/glass_a.vmat" );
		foreach ( var back in DormersBothSides ? new[] { false, true } : new[] { false } )
		{
			var foot = back ? y1 - (y1 - middle) * 0.4f : y0 + run * 0.4f;
			for ( var i = 0; i < Dormers; i++ )
			{
				var center = slot * (i + 0.5f);
				var overhang = Math.Clamp( DormerOverhang, 0, 32 );
				var width = MathF.Min( DormerWidth, slot - 4.0f - overhang * 2.0f );
				if ( width < 8.0f )
					continue;

				var spec = new DormerSpec
				{
					Shape = DormerRoof,
					Width = width,
					WallHeight = DormerHeight,
					Pitch = pitch,
					RoofRise = Math.Clamp( DormerRoofRise, 2, 128 ),
					Overhang = overhang,
					Window = DormerWindow,
					WindowWidth = DormerWindowWidth,
					WindowHeight = DormerWindowHeight,
					Flashing = DormerFlashing,
					BodyMaterial = TrimMaterial,
					RoofMaterial = SurfaceMaterial,
					TrimMaterial = TrimMaterial,
					Frame = new WindowSpec { Width = 2.0f, Depth = 2.5f, Corner = DormerWindowCorner, Detail = DormerWindowDetail, FrameMaterial = TrimMaterial, GlassMaterial = glass, DetailMaterial = TrimMaterial }
				};
				// Stay below the ridge and its cap.
				DormerKit.Fit( spec, RidgeHeight * 0.6f - 3.0f * tan - 3.0f );
				kit.Frame = frame.ToWorld( new Transform( new Vector3( center, foot, floor ), back ? Rotation.FromYaw( 180.0f ) : Rotation.Identity ) );
				kit.Material = TrimMaterial;
				var r = DormerKit.Build( kit, spec );
				_dormerFootprints.Add( back
					? (center - r.HalfSpan - 1.0f, center + r.HalfSpan + 1.0f, foot - r.Back - 1.0f, foot - r.Front + 1.0f)
					: (center - r.HalfSpan - 1.0f, center + r.HalfSpan + 1.0f, foot + r.Front - 1.0f, foot + r.Back + 1.0f) );
			}
		}

		kit.Frame = frame;
	}

	void BuildRoofBodyMesh( MeshKit kit, int mask = 0 )
	{
		RefreshWallThickness();
		var frame = RectFrame;
		frame.Position += Vector3.Up * Elevation;
		RoofBounds( out var x0, out var x1, out var y0, out var y1 );
		var middle = (y0 + y1) * 0.5f;
		var ridge = EdgeThickness + RidgeHeight;
		kit.Frame = frame;
		if ( mask != 2 )
		{
			if ( Hollow && Shape != RoofShape.Flat )
			{
				BuildHollowRoof( kit, frame, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, ShellThickness, SurfaceMaterial, RoofInteriorSurface ?? TrimMaterial, CustomProfile );
				if ( BottomClosure )
				{
					kit.Frame = frame;
					kit.Material = TrimMaterial;
					kit.Box( new Vector3( -MathF.Max( WallMargin, 0 ), -MathF.Max( WallMargin, 0 ), -Math.Clamp( BottomThickness, .25f, 128 ) ), new Vector3( RectLength + MathF.Max( WallMargin, 0 ), RectDepth + MathF.Max( WallMargin, 0 ), 0 ) );
				}
			}
			else
				BuildRoof( kit, frame, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, SurfaceMaterial, TrimMaterial, CustomProfile );
		}

		if ( mask == 1 )
			return;
		if ( Hollow && Shape != RoofShape.Flat && FitToWalls )
			BuildWallSeat( kit, frame, RectLength, RectDepth, WallMargin > 0 ? WallMargin : 2, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, ShellThickness, SurfaceMaterial, WallClosureSurface ?? TrimMaterial, CustomProfile, WallClosureThickness );
		kit.Frame = frame;
		kit.Material = TrimMaterial;
		if ( Gutters )
		{
			var projection = Math.Clamp( GutterProjection, 0, 32 );
			var width = MathF.Max( Math.Clamp( GutterWidth, 1, 24 ), Downpipes ? 2.25f * Math.Clamp( DownpipeRadius, .5f, 12 ) + 2 * Math.Clamp( GutterWallThickness, .25f, 4 ) + .5f : 1 );
			var height = Math.Clamp( GutterHeight, 1, 24 );
			var z = EdgeThickness + Math.Clamp( GutterVerticalOffset, -32, 32 );
			AddRoofDrainage( kit, frame, x0 - (GutterEndReturns ? width * .5f : 0), x1 + (GutterEndReturns ? width * .5f : 0), y0 - projection - width * .5f, y1 + projection + width * .5f, z, width, height );
		}

		AddEdgeTrim( kit, frame, x0, x1, y0, y1 );
		if ( Shape != RoofShape.Gable )
			return;
		if ( RidgeCap )
			kit.Beam( new Vector3( x0, middle, ridge ), new Vector3( x1, middle, ridge ), 6.0f, 3.0f );
		BuildDormers( kit, frame, y0, y1, middle );
		kit.Frame = frame;
		kit.Material = TrimMaterial;
		for ( var row = 1; row <= TileRows; row++ )
		{
			var t = row / (TileRows + 1f);
			var z = EdgeThickness + RidgeHeight * t + 0.4f;
			TileRow( kit, x0, x1, y0 + (middle - y0) * t, z );
			TileRow( kit, x0, x1, y1 + (middle - y1) * t, z );
		}

		kit.Frame = frame;
	}

	/// <summary>
	/// Extrude a cross section, given as (y, z) counter clockwise from the front, along the ridge.
	/// </summary>
	static void Section( MeshKit kit, Transform frame, IReadOnlyList<Vector2> section, float x0, float x1 )
	{
		kit.Frame = frame.ToWorld( new Transform( Vector3.Zero, Rotation.FromYaw( 90.0f ) ) );
		kit.Profile( section, -x1, -x0 );
		kit.Frame = frame;
	}

	/// <summary>
	/// Add a closed roof body. +X runs along the ridge. A hip ridge turns to the longer side.
	/// </summary>
	/// <summary>
	/// A round roof over the ellipse inside the rectangle: the profile runs from the rim
	/// (radius share 1) to the top (radius share 0), heights in roof units.
	/// </summary>
	static void Revolve( MeshKit kit, float x0, float x1, float y0, float y1, IReadOnlyList<Vector2> profile, int sides, Material surface, Material trim )
	{
		var cx = (x0 + x1) * 0.5f;
		var cy = (y0 + y1) * 0.5f;
		var rx = (x1 - x0) * 0.5f;
		var ry = (y1 - y0) * 0.5f;

		Vector3[] Ring( Vector2 p ) => [.. Enumerable.Range( 0, sides ).Select( i =>
		{
			var angle = MathF.PI * 2.0f * i / sides;
			return new Vector3( cx + MathF.Cos( angle ) * rx * p.x, cy + MathF.Sin( angle ) * ry * p.x, p.y );
		} )];

		kit.BeginPart();
		kit.Material = trim;

		var rings = profile.Select( Ring ).ToList();
		kit.Face( [.. rings[0].Select( v => v.WithZ( 0.0f ) ).Reverse()] );

		for ( var i = 0; i < sides; i++ )
		{
			var j = (i + 1) % sides;
			kit.Face( rings[0][i].WithZ( 0.0f ), rings[0][j].WithZ( 0.0f ), rings[0][j], rings[0][i] );
		}

		kit.Material = surface;

		for ( var k = 0; k + 1 < rings.Count; k++ )
		{
			var top = profile[k + 1].x <= 0.0001f;

			for ( var i = 0; i < sides; i++ )
			{
				var j = (i + 1) % sides;

				if ( top ) kit.Face( rings[k][i], rings[k][j], rings[k + 1][0] );
				else kit.Face( rings[k][i], rings[k][j], rings[k + 1][j], rings[k + 1][i] );
			}
		}
	}

	public static void BuildRoof( MeshKit kit, Transform frame, float x0, float x1, float y0, float y1,
		RoofShape shape, float rise, float edge, Material surface, Material trim, IReadOnlyList<Vector2> custom = null, bool flushCross = false )
	{
		if ( shape == RoofShape.Hip && y1 - y0 > x1 - x0 )
		{
			kit.Frame = frame.ToWorld( new Transform( Vector3.Zero, Rotation.FromYaw( 90.0f ) ) );
			(x0, x1, y0, y1) = (y0, y1, -x1, -x0);
		}

		var ridge = edge + rise;
		var middle = (y0 + y1) * 0.5f;
		var width = y1 - y0;

		if ( shape == RoofShape.Flat || rise <= 0.0f )
		{
			kit.Material = surface;
			kit.Box( new Vector3( x0, y0, 0.0f ), new Vector3( x1, y1, edge ) );
			kit.Frame = frame;
			return;
		}

		switch ( shape )
		{
			case RoofShape.Gambrel:
				kit.Material = surface;
				Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, edge ), new( y1 - width * 0.14f, edge + rise * 0.6f ), new( middle, ridge ), new( y0 + width * 0.14f, edge + rise * 0.6f ), new( y0, edge )], x0, x1 );
				return;

			case RoofShape.Saltbox:
				kit.Material = surface;
				Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, edge ), new( y0 + width * 0.35f, ridge ), new( y0, edge + rise * 0.3f )], x0, x1 );
				return;

			case RoofShape.Butterfly:
				kit.Material = surface;
				Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, ridge ), new( middle, edge ), new( y0, ridge )], x0, x1 );
				return;

			case RoofShape.Barrel:
				kit.Material = surface;
				var barrel = new List<Vector2> { new( y0, 0 ), new( y1, 0 ) };
				for ( var i = 0; i <= 16; i++ )
				{
					var angle = MathF.PI * i / 16.0f;
					barrel.Add( new Vector2( middle + MathF.Cos( angle ) * width * 0.5f, edge + MathF.Sin( angle ) * rise ) );
				}
				Section( kit, frame, barrel, x0, x1 );
				return;

			case RoofShape.Mansard:
				var inset = MathF.Min( width, x1 - x0 ) * 0.14f;
				var kerb = edge + rise * 0.7f;

				kit.Material = trim;
				kit.Box( new Vector3( x0, y0, 0.0f ), new Vector3( x1, y1, edge ) );

				kit.Material = surface;
				kit.Hex( new( x0, y0, edge ), new( x1, y0, edge ), new( x1, y1, edge ), new( x0, y1, edge ),
					new( x0 + inset, y0 + inset, kerb ), new( x1 - inset, y0 + inset, kerb ), new( x1 - inset, y1 - inset, kerb ), new( x0 + inset, y1 - inset, kerb ) );

				var upper = frame.ToWorld( new Transform( Vector3.Up * kerb ) );
				kit.Frame = upper;
				BuildRoof( kit, upper, x0 + inset, x1 - inset, y0 + inset, y1 - inset, RoofShape.Hip, rise * 0.3f, 0.5f, surface, trim );
				kit.Frame = frame;
				return;

			case RoofShape.Conical:
				Revolve( kit, x0, x1, y0, y1, [new( 1.0f, edge ), new( 0.0f, ridge )], 32, surface, trim );
				kit.Frame = frame;
				return;

			case RoofShape.Dome:
				var dome = new List<Vector2> { new( 1.0f, edge ) };
				for ( var i = 1; i <= 8; i++ )
				{
					var angle = MathF.PI * 0.5f * i / 8.0f;
					dome.Add( new Vector2( i == 8 ? 0.0f : MathF.Cos( angle ), edge + MathF.Sin( angle ) * rise ) );
				}
				Revolve( kit, x0, x1, y0, y1, dome, 32, surface, trim );
				kit.Frame = frame;
				return;

			case RoofShape.Gothic:
				// Two arcs that meet in a point, each with a radius of three quarters of the width.
				kit.Material = surface;
				var arc = width * 0.75f;
				var crown = MathF.Sqrt( arc * arc - (width * 0.5f - arc) * (width * 0.5f - arc) );
				var pointed = new List<Vector2> { new( y0, 0 ), new( y1, 0 ) };
				for ( var i = 0; i <= 8; i++ )
				{
					var y = y1 - width * 0.5f * i / 8.0f;
					var dy = y - (y1 - arc);
					pointed.Add( new Vector2( y, edge + MathF.Sqrt( MathF.Max( arc * arc - dy * dy, 0.0f ) ) / crown * rise ) );
				}
				for ( var i = 7; i >= 0; i-- )
				{
					var y = y0 + width * 0.5f * i / 8.0f;
					var dy = y - (y0 + arc);
					pointed.Add( new Vector2( y, edge + MathF.Sqrt( MathF.Max( arc * arc - dy * dy, 0.0f ) ) / crown * rise ) );
				}
				Section( kit, frame, pointed, x0, x1 );
				return;

			case RoofShape.GullWing:
				kit.Material = surface;
				Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, edge ), new( y1 - width * 0.3f, edge + rise * 0.3f ), new( middle, ridge ), new( y0 + width * 0.3f, edge + rise * 0.3f ), new( y0, edge )], x0, x1 );
				return;

			case RoofShape.Bonnet:
				// A shallow skirt at the eaves under a steep hip, the other way round from a mansard.
				var skirt = MathF.Min( width, x1 - x0 ) * 0.15f;
				var kick = edge + rise * 0.2f;

				kit.Material = trim;
				kit.Box( new Vector3( x0, y0, 0.0f ), new Vector3( x1, y1, edge ) );

				kit.Material = surface;
				kit.Hex( new( x0, y0, edge ), new( x1, y0, edge ), new( x1, y1, edge ), new( x0, y1, edge ),
					new( x0 + skirt, y0 + skirt, kick ), new( x1 - skirt, y0 + skirt, kick ), new( x1 - skirt, y1 - skirt, kick ), new( x0 + skirt, y1 - skirt, kick ) );

				var bonnet = frame.ToWorld( new Transform( Vector3.Up * kick ) );
				kit.Frame = bonnet;
				BuildRoof( kit, bonnet, x0 + skirt, x1 - skirt, y0 + skirt, y1 - skirt, RoofShape.Hip, ridge - kick - 0.5f, 0.5f, surface, trim );
				kit.Frame = frame;
				return;

			case RoofShape.Custom:
				var given = (custom ?? []).Where( p => p.x >= 0.0f && p.x <= 1.0f ).OrderBy( p => p.x ).ToList();

				// Without a usable profile the roof stays a gable rather than nothing.
				if ( given.Count < 2 )
				{
					BuildRoof( kit, frame, x0, x1, y0, y1, RoofShape.Gable, rise, edge, surface, trim );
					return;
				}

				kit.Material = surface;
				var outline = new List<Vector2> { new( y0, 0 ), new( y1, 0 ), new( y1, edge ) };
				for ( var i = given.Count - 1; i >= 0; i-- )
				{
					var p = given[i];
					if ( (i == given.Count - 1 && p.x >= 0.999f && p.y <= 0.001f) || (i == 0 && p.x <= 0.001f && p.y <= 0.001f) ) continue;
					outline.Add( new Vector2( y0 + width * p.x, edge + Math.Clamp( p.y, 0.0f, 1.0f ) * rise ) );
				}
				outline.Add( new Vector2( y0, edge ) );
				Section( kit, frame, outline, x0, x1 );
				return;

			case RoofShape.Monitor:
				kit.Material = surface;
				Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, edge ), new( y1 - width * 0.3f, edge + rise * 0.45f ), new( y1 - width * 0.3f, edge + rise * 0.75f ),
					new( middle, ridge ), new( y0 + width * 0.3f, edge + rise * 0.75f ), new( y0 + width * 0.3f, edge + rise * 0.45f ), new( y0, edge )], x0, x1 );
				return;

			case RoofShape.Sawtooth:
				kit.Material = surface;
				var teeth = Math.Clamp( (int)MathF.Round( width / MathF.Max( rise * 2.0f, 1.0f ) ), 2, 8 );
				var tooth = width / teeth;
				var saw = new List<Vector2> { new( y0, 0 ), new( y1, 0 ), new( y1, edge ) };

				// Each tooth rises from its low edge to a vertical glazed face, the way north lights do.
				for ( var k = 0; k < teeth; k++ )
				{
					saw.Add( new Vector2( y1 - tooth * k, ridge ) );
					saw.Add( new Vector2( y1 - tooth * (k + 1), edge ) );
				}

				Section( kit, frame, saw, x0, x1 );
				return;

			case RoofShape.MShape:
				kit.Material = surface;
				Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, edge ), new( y1 - width * 0.25f, ridge ), new( middle, edge + rise * 0.15f ), new( y0 + width * 0.25f, ridge ), new( y0, edge )], x0, x1 );
				return;

			case RoofShape.Bellcast:
				kit.Material = surface;
				Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, edge ), new( y1 - width * 0.1f, edge + rise * 0.1f ), new( y1 - width * 0.2f, edge + rise * 0.3f ),
					new( middle, ridge ), new( y0 + width * 0.2f, edge + rise * 0.3f ), new( y0 + width * 0.1f, edge + rise * 0.1f ), new( y0, edge )], x0, x1 );
				return;

			case RoofShape.Clerestory:
				kit.Material = surface;
				Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, edge ), new( middle, edge + rise * 0.5f ), new( middle, ridge ), new( y0, edge + rise * 0.7f )], x0, x1 );
				return;

			case RoofShape.DutchGable:
				// A hip roof up to two thirds, a small gable on top of it.
				var step = width * 0.5f * 0.6f;
				var level = edge + rise * 0.6f;

				kit.Material = trim;
				kit.Box( new Vector3( x0, y0, 0.0f ), new Vector3( x1, y1, edge ) );

				kit.Material = surface;
				kit.Hex( new( x0, y0, edge ), new( x1, y0, edge ), new( x1, y1, edge ), new( x0, y1, edge ),
					new( x0 + step, y0 + step, level ), new( x1 - step, y0 + step, level ), new( x1 - step, y1 - step, level ), new( x0 + step, y1 - step, level ) );

				var gablet = frame.ToWorld( new Transform( Vector3.Up * level ) );
				kit.Frame = gablet;
				BuildRoof( kit, gablet, x0 + step, x1 - step, y0 + step, y1 - step, RoofShape.Gable, ridge - level - 0.5f, 0.5f, surface, trim );
				kit.Frame = frame;
				return;

			case RoofShape.CrossGable:
				BuildRoof( kit, frame, x0, x1, y0, y1, RoofShape.Gable, rise, edge, surface, trim );

				var cross = frame.ToWorld( new Transform( new Vector3( (x0 + x1) * 0.5f, 0.0f, 0.0f ), Rotation.FromYaw( 90.0f ) ) );
				var half = (x1 - x0) * 0.2f;
				// On its own the cross wing reaches out past the roof; on a house it ends with the eaves.
				var reach = (y1 - y0) * 0.5f + (flushCross ? 0.0f : half);

				kit.Frame = cross;
				BuildRoof( kit, cross, middle - reach, middle + reach, -half, half, RoofShape.Gable, MathF.Min( rise, rise * half * 2.0f / width + rise * 0.5f ), edge, surface, trim );
				kit.Frame = frame;
				return;
		}

		kit.BeginPart();
		kit.Material = trim;
		kit.FacingDown( x0, x1, y0, y1, 0.0f );

		switch ( shape )
		{
			case RoofShape.Shed:
				kit.FacingFront( x0, x1, y0, 0.0f, edge );
				kit.FacingBack( x0, x1, y1, 0.0f, ridge );
				kit.Face( new( x0, y1, 0.0f ), new( x0, y0, 0.0f ), new( x0, y0, edge ), new( x0, y1, ridge ) );
				kit.Face( new( x1, y0, 0.0f ), new( x1, y1, 0.0f ), new( x1, y1, ridge ), new( x1, y0, edge ) );

				kit.Material = surface;
				kit.Face( new( x0, y0, edge ), new( x1, y0, edge ), new( x1, y1, ridge ), new( x0, y1, ridge ) );
				break;

			case RoofShape.Gable:
				kit.FacingFront( x0, x1, y0, 0.0f, edge );
				kit.FacingBack( x0, x1, y1, 0.0f, edge );
				kit.Face( new( x0, y1, 0.0f ), new( x0, y0, 0.0f ), new( x0, y0, edge ), new( x0, middle, ridge ), new( x0, y1, edge ) );
				kit.Face( new( x1, y0, 0.0f ), new( x1, y1, 0.0f ), new( x1, y1, edge ), new( x1, middle, ridge ), new( x1, y0, edge ) );

				kit.Material = surface;
				kit.Face( new( x0, y0, edge ), new( x1, y0, edge ), new( x1, middle, ridge ), new( x0, middle, ridge ) );
				kit.Face( new( x1, y1, edge ), new( x0, y1, edge ), new( x0, middle, ridge ), new( x1, middle, ridge ) );
				break;

			case RoofShape.Pyramid:
				var apex = new Vector3( (x0 + x1) * 0.5f, middle, ridge );

				kit.FacingFront( x0, x1, y0, 0.0f, edge );
				kit.FacingBack( x0, x1, y1, 0.0f, edge );
				kit.FacingRearward( x0, y0, y1, 0.0f, edge );
				kit.FacingForward( x1, y0, y1, 0.0f, edge );

				kit.Material = surface;
				kit.Face( new( x0, y0, edge ), new( x1, y0, edge ), apex );
				kit.Face( new( x1, y1, edge ), new( x0, y1, edge ), apex );
				kit.Face( new( x0, y1, edge ), new( x0, y0, edge ), apex );
				kit.Face( new( x1, y0, edge ), new( x1, y1, edge ), apex );
				break;

			case RoofShape.Jerkinhead:
				// A gable whose top is cut back into a small hip at both ends.
				var clip = 0.7f;
				var clipZ = edge + rise * clip;
				var yA = y0 + (middle - y0) * clip;
				var yB = y1 - (y1 - middle) * clip;
				var cut = middle - yA;

				kit.FacingFront( x0, x1, y0, 0.0f, edge );
				kit.FacingBack( x0, x1, y1, 0.0f, edge );
				kit.Face( new( x0, y1, 0.0f ), new( x0, y0, 0.0f ), new( x0, y0, edge ), new( x0, yA, clipZ ), new( x0, yB, clipZ ), new( x0, y1, edge ) );
				kit.Face( new( x1, y0, 0.0f ), new( x1, y1, 0.0f ), new( x1, y1, edge ), new( x1, yB, clipZ ), new( x1, yA, clipZ ), new( x1, y0, edge ) );

				kit.Material = surface;
				kit.Face( new( x0, y0, edge ), new( x1, y0, edge ), new( x1, yA, clipZ ), new( x1 - cut, middle, ridge ), new( x0 + cut, middle, ridge ), new( x0, yA, clipZ ) );
				kit.Face( new( x1, y1, edge ), new( x0, y1, edge ), new( x0, yB, clipZ ), new( x0 + cut, middle, ridge ), new( x1 - cut, middle, ridge ), new( x1, yB, clipZ ) );
				kit.Face( new( x0, yB, clipZ ), new( x0, yA, clipZ ), new( x0 + cut, middle, ridge ) );
				kit.Face( new( x1, yA, clipZ ), new( x1, yB, clipZ ), new( x1 - cut, middle, ridge ) );
				break;

			case RoofShape.Hip:
				var hip = (y1 - y0) * 0.5f;

				kit.FacingFront( x0, x1, y0, 0.0f, edge );
				kit.FacingBack( x0, x1, y1, 0.0f, edge );
				kit.FacingRearward( x0, y0, y1, 0.0f, edge );
				kit.FacingForward( x1, y0, y1, 0.0f, edge );

				kit.Material = surface;
				kit.Face( new( x0, y0, edge ), new( x1, y0, edge ), new( x1 - hip, middle, ridge ), new( x0 + hip, middle, ridge ) );
				kit.Face( new( x1, y1, edge ), new( x0, y1, edge ), new( x0 + hip, middle, ridge ), new( x1 - hip, middle, ridge ) );
				kit.Face( new( x0, y1, edge ), new( x0, y0, edge ), new( x0 + hip, middle, ridge ) );
				kit.Face( new( x1, y0, edge ), new( x1, y1, edge ), new( x1 - hip, middle, ridge ) );
				break;
		}

		kit.Frame = frame;
	}
}