editor/AdvancedMappingTools/Procedural/DormerGenerator.cs

Placeholder

Rough Code
namespace AdvancedMappingTools;

public enum DormerStyle
{
	[Title( "Gable" )] Gable,
	[Title( "Shed" )] Shed,
	Flat,
	Hip,
	[Title( "Barrel (Round)" )] Barrel,
	[Title( "Segmental Arch" )] Segmental
}

public sealed class DormerSettings : PropSettings
{
	[Property, Group( "Dormer" ), Title( "Style" ), WideMode]
	public DormerStyle Style { get; set; } = DormerStyle.Gable;

	[Property, Group( "Dormer" ), Range( 16, 512, slider: false ), WideMode]
	public float Width { get; set; } = 64.0f;

	[Property, Group( "Dormer" ), Range( 8, 512, slider: false ), WideMode]
	public float Depth { get; set; } = 56.0f;

	[Property, Group( "Dormer" ), Range( 8, 512, slider: false ), WideMode]
	public float Height { get; set; } = 56.0f;

	[Property, Group( "Dormer" ), Title( "Roof Rise" ), Range( 2, 256, slider: false ), WideMode]
	public float RoofRise { get; set; } = 24.0f;

	[Property, Group( "Dormer" ), Title( "Overhang" ), Range( 0, 96, slider: false ), WideMode]
	public float Overhang { get; set; } = 6.0f;

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

	[Property, ShowIf( nameof( Window ), true ), Group( "Window" ), Title( "Round" ), WideMode, Description( "A round window in the front face instead of a rectangular one." )]
	public bool RoundWindow { get; set; } = false;

	[Property, ShowIf( nameof( Window ), true ), Group( "Window" ), Title( "Window Width" ), Range( 4, 512, slider: false ), WideMode]
	public float WindowWidth { get; set; } = 36.0f;

	[Property, ShowIf( nameof( Window ), true ), Group( "Window" ), Title( "Window Height" ), Range( 4, 512, slider: false ), WideMode]
	public float WindowHeight { get; set; } = 36.0f;

	[Property, Group( "Dormer" ), Title( "Sits On A Sloped Roof" ), WideMode, Description( "Build the dormer for a roof of the given pitch: the cheeks and its own roof end exactly in the roof surface, the window sits in a real reveal, with sill and flashing. Off: the free-standing box of earlier versions." )]
	public bool OnSlope { get; set; }

	[Property, Group( "Dormer" ), Title( "Main Roof Pitch" ), Range( 5, 75, slider: false ), WideMode, Description( "Pitch of the roof the dormer stands on, in degrees. Place the dormer with its front facing down the slope." )]
	public float RoofPitch { get; set; } = 35.0f;

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

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

	[Property, Group( "Window" ), Title( "Frame Corners" ), WideMode]
	public FrameCorner WindowCorner { get; set; } = FrameCorner.Mitred;

	[Property, Group( "Window" ), Title( "Frame Profile" ), WideMode]
	public FrameProfile WindowProfile { get; set; } = FrameProfile.Chamfered;

	[Property, Group( "Window" ), Title( "Frame Detail" ), WideMode]
	public WindowDetail WindowDetail { get; set; } = WindowDetail.Standard;

	[Property, Group( "Window" ), Title( "Glazing" ), WideMode]
	public Glazing WindowGlazing { get; set; } = Glazing.Double;

	[Property, Group( "Window" ), Title( "Panes Across" ), Range( 1, 6, slider: false ), WideMode]
	public int WindowPanesAcross { get; set; } = 1;

	[Property, Group( "Window" ), Title( "Panes Up" ), Range( 1, 6, slider: false ), WideMode]
	public int WindowPanesUp { get; set; } = 1;

	public override string PropName => Style.ToString();

	public override IReadOnlySet<string> UnusedOptions
	{
		get
		{
			var hidden = base.UnusedOptions is { } shown ? new HashSet<string>( shown ) : new HashSet<string>();
			string[] slope = [nameof( RoofPitch ), nameof( Flashing ), nameof( WindowSill ), nameof( WindowCorner ), nameof( WindowProfile ), nameof( WindowDetail ), nameof( WindowGlazing ), nameof( WindowPanesAcross ), nameof( WindowPanesUp )];
			// The new roof shapes exist only as dormers for a sloped roof.
			if ( !OnSlope && Style is DormerStyle.Gable or DormerStyle.Shed )
				hidden.UnionWith( slope );
			if ( Style is not (DormerStyle.Gable or DormerStyle.Shed) )
				hidden.Add( nameof( OnSlope ) );
			else
			{
				// On a slope the depth follows from the pitch, and windows are always rectangular.
				hidden.UnionWith( [nameof( Depth ), nameof( RoundWindow )] );
				if ( !Window )
					hidden.UnionWith( [nameof( WindowSill ), nameof( WindowCorner ), nameof( WindowProfile ), nameof( WindowDetail ), nameof( WindowGlazing ), nameof( WindowPanesAcross ), nameof( WindowPanesUp )] );
				if ( Style is DormerStyle.Shed or DormerStyle.Flat or DormerStyle.Barrel )
					hidden.Add( nameof( RoofRise ) );
			}

			return hidden;
		}
	}
}

/// <summary>
/// A dormer that sits on a roof: a small body with a window and a gable or shed roof of its
/// own. The window looks out of the front face; the body is open at the back so it can meet
/// the roof plane.
/// </summary>
public sealed class DormerGenerator : PropGenerator
{
	public override string Id => "dormer";
	public override string Title => "Dormer";
	public override Type SettingsType => typeof( DormerSettings );

	protected override void Build( PropSettings settings, PropBuilder b )
	{
		var s = (DormerSettings)settings;
		var width = MathF.Max( s.Width, 8.0f );
		var depth = MathF.Max( s.Depth, 8.0f );
		var height = MathF.Max( s.Height, 8.0f );
		var half = width * 0.5f;
		var deep = depth * 0.5f;
		var over = MathF.Max( s.Overhang, 0.0f );
		var rise = MathF.Max( s.RoofRise, 2.0f );

		if ( s.OnSlope || s.Style is not (DormerStyle.Gable or DormerStyle.Shed) )
		{
			OnSlope( s, b );
			return;
		}

		b.Box( "body", "Body", b.Main, new( -half, -deep, 0.0f ), new( half, deep, height ) );

		if ( s.Window )
		{
			if ( s.RoundWindow )
			{
				var radius = MathF.Min( MathF.Min( s.WindowWidth, s.WindowHeight ), MathF.Min( width, height ) * 0.4f ) * 0.5f;

				b.Disc( "window", "Window", b.Detail, 0.0f, height * 0.55f, radius, -deep - 1.2f, -deep, 16 );
				b.Disc( "window_ring", "Ring", b.Metal, 0.0f, height * 0.55f, radius + 2.0f, -deep - 1.4f, -deep - 1.2f, 16 );
			}
			else
			{
				var w = MathF.Min( s.WindowWidth, width * 0.8f );
				var h = MathF.Min( s.WindowHeight, height * 0.75f );
				var bottom = height * 0.3f;
				var left = -w * 0.5f;
				var right = w * 0.5f;

				b.Box( "window", "Window", b.Detail, new( left, -deep - 1.2f, bottom ), new( right, -deep, bottom + h ) );
				b.Box( "frame_top", "Frame", b.Metal, new( left - 2.0f, -deep - 1.4f, bottom + h ), new( right + 2.0f, -deep, bottom + h + 2.0f ) );
				b.Box( "frame_bottom", "Frame", b.Metal, new( left - 2.0f, -deep - 1.4f, bottom - 2.0f ), new( right + 2.0f, -deep, bottom ) );
				b.Box( "frame_left", "Frame", b.Metal, new( left - 2.0f, -deep - 1.4f, bottom ), new( left, -deep, bottom + h ) );
				b.Box( "frame_right", "Frame", b.Metal, new( right, -deep - 1.4f, bottom ), new( right + 2.0f, -deep, bottom + h ) );
			}
		}

		if ( s.Style == DormerStyle.Gable )
		{
			var roof = new List<Vector2> { new( -half - over, height ), new( half + over, height ), new( 0.0f, height + rise ) };

			b.Side( "roof", "Roof", b.Metal, roof, -deep - over, deep + over );
		}
		else
		{
			var roof = new List<Vector2>
			{
				new( -half - over, height ),
				new( half + over, height + rise ),
				new( half + over, height + rise + 3.0f ),
				new( -half - over, height + 3.0f )
			};

			b.Side( "roof", "Roof", b.Metal, roof, -deep - over, deep + over );
		}
	}

	/// <summary>
	/// The dormer for a sloped roof from <see cref="DormerKit"/>, each piece its own part. The front
	/// wall stands at y = -Depth / 2 as in the box version; the roof surface rises toward +Y.
	/// </summary>
	static void OnSlope( DormerSettings s, PropBuilder b )
	{
		var spec = new DormerSpec
		{
			Shape = s.Style switch
			{
				DormerStyle.Shed => DormerShape.Shed,
				DormerStyle.Flat => DormerShape.Flat,
				DormerStyle.Hip => DormerShape.Hip,
				DormerStyle.Barrel => DormerShape.Barrel,
				DormerStyle.Segmental => DormerShape.Segmental,
				_ => DormerShape.Gable
			},
			Width = MathF.Max( s.Width, 8.0f ),
			WallHeight = MathF.Max( s.Height, 8.0f ),
			Pitch = Math.Clamp( s.RoofPitch, 5, 75 ),
			RoofRise = MathF.Max( s.RoofRise, 2.0f ),
			Overhang = MathF.Max( s.Overhang, 0.0f ),
			Window = s.Window,
			WindowWidth = s.WindowWidth,
			WindowHeight = s.WindowHeight,
			WindowSill = 6,
			Sill = s.WindowSill,
			Flashing = s.Flashing,
			BodyMaterial = b.Main,
			RoofMaterial = b.Metal,
			TrimMaterial = b.Metal,
			Frame = new WindowSpec
			{
				Width = 2.0f,
				Depth = 2.5f,
				Corner = s.WindowCorner,
				Profile = s.WindowProfile,
				Detail = s.WindowDetail,
				Glazing = s.WindowGlazing,
				Columns = Math.Clamp( s.WindowPanesAcross, 1, 6 ),
				Rows = Math.Clamp( s.WindowPanesUp, 1, 6 ),
				FrameMaterial = b.Metal,
				GlassMaterial = b.Detail,
				DetailMaterial = b.Main
			}
		};
		var front = -MathF.Max( s.Depth, 8.0f ) * 0.5f;
		void Piece( string id, string label, Material material, DormerPieces piece ) => b.Part( id, label, material, kit =>
		{
			kit.Frame = kit.Frame.ToWorld( new Transform( new Vector3( 0, front, 0 ) ) );
			DormerKit.Build( kit, spec, piece );
		} );

		Piece( "body", "Body", b.Main, DormerPieces.Body );
		if ( s.Window )
		{
			Piece( "window", "Window", b.Metal, DormerPieces.Window );
			if ( s.WindowSill )
				Piece( "sill", "Window Sill", b.Metal, DormerPieces.Sill );
		}

		Piece( "roof", "Roof", b.Metal, DormerPieces.Roof );
		if ( s.Flashing )
			Piece( "flashing", "Flashing", b.Metal, DormerPieces.Flashing );
	}
}