editor/AdvancedMappingTools/Procedural/RoofGenerator.cs

A procedural roof generator for the Advanced Mapping Tools editor. Builds roof mesh parts (panels, gable ends, ridges, rafters, gutters, fascia) for ~24 roof styles from a rectangle and a RoofRequest, delegating complex styles to RoofEditor.BuildRoof.

namespace AdvancedMappingTools;

public enum HouseRoof
{
	Gable,
	Hip,
	Shed,
	Flat,
	[Title( "Pyramid" )] Pyramid,
	[Title( "Butterfly" )] Butterfly,
	Mansard,
	Gambrel,
	Saltbox,
	Barrel,
	[Title( "Cross Gable" )] CrossGable,
	[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
}

public sealed record RoofRequest(
	string Id, string Name, float Width, float Depth, float Rise, float Overhang, float Thickness,
	HouseRoof Shape, float RafterSpacing, bool Gutters, float Variation, Material RoofMaterial, Material TrimMaterial,
	bool RidgeCap = true, bool Fascia = false, float EndOverhang = -1.0f, float GableThickness = 0.0f );

/// <summary>
/// A roof over the rectangle from (0, 0) to (width, depth) at height zero, built from separate
/// panels, gable ends, ridge, rafters and gutters. The ridge runs along +X.
/// Everything random comes from the roof seed, so a new roof leaves the house alone.
/// </summary>
public static class RoofGenerator
{
	public const string Category = "roof";

	const float GableThickness = 4.0f;
	const float BeyondEnds = 0.15f;

	/// <summary>
	/// How far the panels sink into the walls that carry them, so no seam shows between the two.
	/// Less than the thinnest roof, the top of a wall stays hidden inside the panel.
	/// </summary>
	const float Seat = 0.5f;

	public static PartPlan Generate( RoofRequest request, GenContext context )
	{
		var group = context.Group( request.Id, request.Name, "RoofAssembly", Category );
		var stream = context.Stream( Category, request.Id );

		var rise = request.Rise * (1.0f + request.Variation * stream.Range( -0.4f, 0.4f ));
		var spread = 1.0f + request.Variation * stream.Range( -0.4f, 0.4f );
		var overhang = request.Overhang * spread;
		var spacing = MathF.Max( request.RafterSpacing, 8.0f );
		var phase = spacing * stream.Next();

		// The gable ends may reach out less or more than the eaves; minus one keeps them alike.
		var endOverhang = request.EndOverhang < 0.0f ? overhang : request.EndOverhang * spread;
		var x0 = -endOverhang;
		var x1 = request.Width + endOverhang;
		var y0 = -overhang;
		var y1 = request.Depth + overhang;
		var t = request.Thickness;
		var middle = (y0 + y1) * 0.5f;
		// A gable as thick as the wall below carries that wall on up, flush on the outside.
		var wall = MathF.Min( request.GableThickness > 0.0f ? request.GableThickness : GableThickness, MathF.Min( request.Width, request.Depth ) * 0.25f );

		// The slope runs from the wall to the ridge. Past the wall the panels carry on at that slope,
		// so the underside rests on the top of the wall and the eaves hang below it.
		var run = request.Shape switch
		{
			HouseRoof.Shed => request.Depth,
			HouseRoof.Hip => MathF.Min( request.Width, request.Depth ) * 0.5f,
			_ => request.Depth * 0.5f
		};
		var drop = overhang * rise / run;
		var eave = -drop - Seat;
		var ridge = rise - Seat;

		PartPlan Part( string name, string label, string kind, Material material, Action<MeshKit> build ) =>
			group.Add( context.Solid( $"{request.Id}/{name}", label, kind, Category, material, build ) );

		// The underside of a roof with broken lines, from the front eave to the back eave.
		List<Vector2> line = null;

		switch ( request.Shape )
		{
			case HouseRoof.Flat:
				Part( "slab", "Roof Slab", "RoofPanel", request.RoofMaterial, kit => kit.Box( new Vector3( x0, y0, 0.0f ), new Vector3( x1, y1, t ) ) );
				Part( "parapet_front", "Front Parapet", "Parapet", request.TrimMaterial, kit => kit.Box( new Vector3( x0, y0, t ), new Vector3( x1, y0 + 4.0f, t + 12.0f ) ) );
				Part( "parapet_back", "Back Parapet", "Parapet", request.TrimMaterial, kit => kit.Box( new Vector3( x0, y1 - 4.0f, t ), new Vector3( x1, y1, t + 12.0f ) ) );
				Part( "parapet_left", "Left Parapet", "Parapet", request.TrimMaterial, kit => kit.Box( new Vector3( x0, y0 + 4.0f, t ), new Vector3( x0 + 4.0f, y1 - 4.0f, t + 12.0f ) ) );
				Part( "parapet_right", "Right Parapet", "Parapet", request.TrimMaterial, kit => kit.Box( new Vector3( x1 - 4.0f, y0 + 4.0f, t ), new Vector3( x1, y1 - 4.0f, t + 12.0f ) ) );
				return group;

			case HouseRoof.Shed:
				var high = ridge + drop;
				var inner = rise * (request.Depth - wall) / request.Depth;

				Part( "panel", "Roof Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x0, y0, eave ), new( x1, y0, eave ), new( x1, y1, high ), new( x0, y1, high ), t ) );
				Part( "wall_left", "Left Gable", "GableEnd", request.TrimMaterial, kit => Wedge( kit, 0.0f, wall, 0.0f, request.Depth, request.Depth, rise ) );
				Part( "wall_right", "Right Gable", "GableEnd", request.TrimMaterial, kit => Wedge( kit, request.Width - wall, request.Width, 0.0f, request.Depth, request.Depth, rise ) );

				// The high side would stand open without this, it fits between the two gables.
				Part( "wall_back", "Back Wall", "GableEnd", request.TrimMaterial, kit => kit.Hex(
					new( wall, request.Depth - wall, 0.0f ), new( request.Width - wall, request.Depth - wall, 0.0f ), new( request.Width - wall, request.Depth, 0.0f ), new( wall, request.Depth, 0.0f ),
					new( wall, request.Depth - wall, inner ), new( request.Width - wall, request.Depth - wall, inner ), new( request.Width - wall, request.Depth, rise ), new( wall, request.Depth, rise ) ) );
				break;

			case HouseRoof.Hip:
				// The ridge follows the longer side and shrinks to a point on a square house.
				var hip = MathF.Min( x1 - x0, y1 - y0 ) * 0.5f;
				var alongX = (x1 - x0) * 0.5f - hip;
				var alongY = (y1 - y0) * 0.5f - hip;
				var center = (x0 + x1) * 0.5f;

				var frontLeft = new Vector3( center - alongX, middle - alongY, ridge );
				var frontRight = new Vector3( center + alongX, middle - alongY, ridge );
				var backRight = new Vector3( center + alongX, middle + alongY, ridge );
				var backLeft = new Vector3( center - alongX, middle + alongY, ridge );

				Part( "panel_front", "Front Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x0, y0, eave ), new( x1, y0, eave ), frontRight, frontLeft, t ) );
				Part( "panel_back", "Back Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x1, y1, eave ), new( x0, y1, eave ), backLeft, backRight, t ) );
				Part( "panel_left", "Left Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x0, y1, eave ), new( x0, y0, eave ), frontLeft, backLeft, t ) );
				Part( "panel_right", "Right Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x1, y0, eave ), new( x1, y1, eave ), backRight, frontRight, t ) );

				if ( alongX + alongY > 0.5f )
					if ( request.RidgeCap ) Part( "ridge", "Ridge Beam", "RidgeBeam", request.TrimMaterial, kit => kit.Beam( frontLeft + Vector3.Up * t, backRight + Vector3.Up * t, 5.0f, 3.0f ) );
				break;

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

				Part( "panel_front", "Front Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x0, y0, eave ), new( x1, y0, eave ), apex, apex, t ) );
				Part( "panel_right", "Right Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x1, y0, eave ), new( x1, y1, eave ), apex, apex, t ) );
				Part( "panel_back", "Back Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x1, y1, eave ), new( x0, y1, eave ), apex, apex, t ) );
				Part( "panel_left", "Left Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x0, y1, eave ), new( x0, y0, eave ), apex, apex, t ) );
				break;

			case HouseRoof.Butterfly:
				// The eaves are the high lines, the valley runs down the middle.
				var eaveHigh = rise;
				var valley = 0.0f;

				Part( "panel_front", "Front Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x0, y0, eaveHigh ), new( x1, y0, eaveHigh ), new( x1, middle, valley ), new( x0, middle, valley ), t ) );
				Part( "panel_back", "Back Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x1, y1, eaveHigh ), new( x0, y1, eaveHigh ), new( x0, middle, valley ), new( x1, middle, valley ), t ) );
				Part( "valley", "Valley Beam", "RidgeBeam", request.TrimMaterial, kit => kit.Beam( new Vector3( x0 - BeyondEnds, middle, valley ), new Vector3( x1 + BeyondEnds, middle, valley ), 5.0f, 3.0f ) );

				line = [new( y0, eaveHigh ), new( middle, valley ), new( y1, eaveHigh )];
				break;

			case HouseRoof.Mansard:
				// Steep lower panels, shallow upper panels and a flat top between them.
				var depth = y1 - y0;
				var breakFront = y0 + depth * 0.22f;
				var breakBack = y1 - depth * 0.22f;
				var topFront = middle - depth * 0.1f;
				var topBack = middle + depth * 0.1f;
				var breakHeight = rise * 0.62f;

				Part( "panel_front_low", "Front Lower Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x0, y0, eave ), new( x1, y0, eave ), new( x1, breakFront, breakHeight ), new( x0, breakFront, breakHeight ), t ) );
				Part( "panel_front_high", "Front Upper Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x0, breakFront, breakHeight ), new( x1, breakFront, breakHeight ), new( x1, topFront, ridge ), new( x0, topFront, ridge ), t ) );
				Part( "panel_back_high", "Back Upper Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x1, breakBack, breakHeight ), new( x0, breakBack, breakHeight ), new( x0, topBack, ridge ), new( x1, topBack, ridge ), t ) );
				Part( "panel_back_low", "Back Lower Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x1, y1, eave ), new( x0, y1, eave ), new( x0, breakBack, breakHeight ), new( x1, breakBack, breakHeight ), t ) );
				Part( "top", "Flat Top", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x0, topFront, ridge ), new( x1, topFront, ridge ), new( x1, topBack, ridge ), new( x0, topBack, ridge ), t ) );

				line = [new( y0, eave ), new( breakFront, breakHeight ), new( topFront, ridge ), new( topBack, ridge ), new( breakBack, breakHeight ), new( y1, eave )];
				break;

			case HouseRoof.Gambrel:
			case HouseRoof.Saltbox:
			case HouseRoof.Barrel:
			case HouseRoof.CrossGable:
			case HouseRoof.Jerkinhead:
			case HouseRoof.DutchGable:
			case HouseRoof.Monitor:
			case HouseRoof.Sawtooth:
			case HouseRoof.MShape:
			case HouseRoof.Bellcast:
			case HouseRoof.Clerestory:
			case HouseRoof.Conical:
			case HouseRoof.Dome:
			case HouseRoof.Gothic:
			case HouseRoof.GullWing:
			case HouseRoof.Bonnet:
				Part( "roof_body", "Roof Body", "RoofPanel", request.RoofMaterial, kit =>
					RoofEditor.BuildRoof( kit, new Transform( Vector3.Zero, Rotation.Identity ), x0, x1, y0, y1,
						Enum.Parse<RoofShape>( request.Shape.ToString() ), rise, t, request.RoofMaterial, request.TrimMaterial, null, flushCross: true ) );
				break;

			default:
				Part( "panel_front", "Front Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x0, y0, eave ), new( x1, y0, eave ), new( x1, middle, ridge ), new( x0, middle, ridge ), t ) );
				Part( "panel_back", "Back Panel", "RoofPanel", request.RoofMaterial, kit => Slope( kit, new( x1, y1, eave ), new( x0, y1, eave ), new( x0, middle, ridge ), new( x1, middle, ridge ), t ) );
				Part( "gable_left", "Left Gable", "GableEnd", request.TrimMaterial, kit => Wedge( kit, 0.0f, wall, 0.0f, request.Depth, middle, rise ) );
				Part( "gable_right", "Right Gable", "GableEnd", request.TrimMaterial, kit => Wedge( kit, request.Width - wall, request.Width, 0.0f, request.Depth, middle, rise ) );
				if ( request.RidgeCap ) Part( "ridge", "Ridge Beam", "RidgeBeam", request.TrimMaterial, kit => kit.Beam( new Vector3( x0 - BeyondEnds, middle, ridge + t ), new Vector3( x1 + BeyondEnds, middle, ridge + t ), 5.0f, 3.0f ) );

				var index = 0;

				for ( var x = x0 + phase; x < x1; x += spacing, index++ )
				{
					var at = x;
					var below = -2.0f;

					Part( $"rafter_front{index:00}", $"Front Rafter {index + 1}", "Rafter", request.TrimMaterial, kit => kit.Beam( new Vector3( at, y0, eave + below ), new Vector3( at, middle, ridge + below ), 2.5f, 4.0f ) );
					// A hair narrower, so the two rafters of a pair have no side in the same place where they meet.
					Part( $"rafter_back{index:00}", $"Back Rafter {index + 1}", "Rafter", request.TrimMaterial, kit => kit.Beam( new Vector3( at, y1, eave + below ), new Vector3( at, middle, ridge + below ), 2.3f, 4.0f ) );
				}
				break;
		}


		if ( line is not null )
		{
			// One piece for every stretch of the line. A wall in one piece would have a hollow
			// outline, and the end faces of a hollow outline cannot be filled cleanly.
			var stops = new List<float> { 0.0f, request.Depth };

			stops.AddRange( line.Select( p => p.x ).Where( y => y > 0.01f && y < request.Depth - 0.01f ) );
			stops.Sort();

			float Top( float y ) => MathF.Max( Under( line, y ) + Seat, Seat );

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

				Part( $"end_left{i}", "End Wall", "GableEnd", request.TrimMaterial, kit => Block( kit, 0.0f, wall, from, to, Top( from ), Top( to ) ) );
				Part( $"end_right{i}", "End Wall", "GableEnd", request.TrimMaterial, kit => Block( kit, request.Width - wall, request.Width, from, to, Top( from ), Top( to ) ) );
			}

			// A butterfly lifts its eaves, so the long sides stand open as well.
			if ( request.Shape == HouseRoof.Butterfly )
			{
				var back = request.Depth - wall;

				Part( "wall_front", "Front Wall", "GableEnd", request.TrimMaterial, kit => Block( kit, wall, request.Width - wall, 0.0f, wall, Top( 0.0f ), Top( wall ) ) );
				Part( "wall_back", "Back Wall", "GableEnd", request.TrimMaterial, kit => Block( kit, wall, request.Width - wall, back, request.Depth, Top( back ), Top( request.Depth ) ) );
			}
		}

		var trimEave = IsBodyShape( request.Shape ) ? 0.0f : eave;

		if ( request.Gutters )
		{
			Part( "gutter_front", "Front Gutter", "Gutter", request.TrimMaterial, kit => kit.Beam( new Vector3( x0 - BeyondEnds, y0 - 1.0f, trimEave ), new Vector3( x1 + BeyondEnds, y0 - 1.0f, trimEave ), 3.0f, 3.0f ) );

			if ( request.Shape != HouseRoof.Shed )
				Part( "gutter_back", "Back Gutter", "Gutter", request.TrimMaterial, kit => kit.Beam( new Vector3( x0 - BeyondEnds, y1 + 1.0f, trimEave ), new Vector3( x1 + BeyondEnds, y1 + 1.0f, trimEave ), 3.0f, 3.0f ) );
		}

		if ( request.Fascia )
		{
			Part( "fascia_front", "Fascia", "FasciaBoard", request.TrimMaterial, kit => kit.Beam( new Vector3( x0 - BeyondEnds, y0, trimEave + t ), new Vector3( x1 + BeyondEnds, y0, trimEave + t ), 2.0f, 6.0f ) );

			if ( request.Shape != HouseRoof.Shed )
				Part( "fascia_back", "Fascia", "FasciaBoard", request.TrimMaterial, kit => kit.Beam( new Vector3( x0 - BeyondEnds, y1, trimEave + t ), new Vector3( x1 + BeyondEnds, y1, trimEave + t ), 2.0f, 6.0f ) );
		}

		return group;
	}

	static bool IsBodyShape( HouseRoof shape ) => (int)shape >= (int)HouseRoof.Gambrel;

	/// <summary>
	/// A sloped slab of some thickness over four corners, counter clockwise from above.
	/// Two equal corners make a triangle.
	/// </summary>
	static void Slope( MeshKit kit, Vector3 a, Vector3 b, Vector3 c, Vector3 d, float thickness )
	{
		var up = Vector3.Up * thickness;
		kit.Hex( a, b, c, d, a + up, b + up, c + up, d + up );
	}

	/// <summary>
	/// The height of a line of (y, z) points at one place across the roof.
	/// </summary>
	static float Under( List<Vector2> line, float y )
	{
		for ( var i = 0; i + 1 < line.Count; i++ )
		{
			var a = line[i];
			var b = line[i + 1];

			if ( y <= b.x || i + 2 == line.Count )
				return a.y + (b.y - a.y) * Math.Clamp( (y - a.x) / MathF.Max( b.x - a.x, 0.001f ), 0.0f, 1.0f );
		}

		return line[^1].y;
	}

	/// <summary>
	/// A piece of wall from height zero up to a sloped top, between x0 and x1 and between y0 and y1.
	/// </summary>
	static void Block( MeshKit kit, float x0, float x1, float y0, float y1, float top0, float top1 ) =>
		kit.Hex(
			new( x0, y0, 0.0f ), new( x1, y0, 0.0f ), new( x1, y1, 0.0f ), new( x0, y1, 0.0f ),
			new( x0, y0, top0 ), new( x1, y0, top0 ), new( x1, y1, top1 ), new( x0, y1, top1 ) );

	/// <summary>
	/// A wall filling the triangle under the roof at one end, from x0 to x1.
	/// </summary>
	static void Wedge( MeshKit kit, float x0, float x1, float y0, float y1, float peak, float height ) =>
		kit.Hex(
			new( x0, y0, 0.0f ), new( x1, y0, 0.0f ), new( x1, y1, 0.0f ), new( x0, y1, 0.0f ),
			new( x0, peak, height ), new( x1, peak, height ), new( x1, peak, height ), new( x0, peak, height ) );
}