Settings class and part generator for the Advanced Mapping Tools procedurally generated house. It lays out a foundation, per storey walls with window and door openings, a staircase, ceiling slabs with a stair opening, and a roof, handing the actual geometry of walls, frames, doors, stairs and roofs to the other generators.
namespace AdvancedMappingTools;
public enum HouseSide
{
Front,
Right,
Back,
Left
}
public enum HouseShutters
{
None,
[Title( "Plain Panels" )] Panel,
[Title( "Louvred" )] Louvred
}
public sealed class HouseSettings : GeneratorSettings, IProfiledFrameSettings
{
public float Width { get; set; } = 384.0f;
public float Depth { get; set; } = 288.0f;
[Property, Group( "House" ), Range( 1, 32, slider: false ), WideMode]
public int Storeys { get; set; } = 2;
[Property, Group( "House" ), Title( "Storey Height" ), Range( 64, 1024, slider: false ), WideMode]
public float StoreyHeight { get; set; } = 128.0f;
[Property, Group( "House" ), Title( "Wall Thickness" ), Range( 1, 64, slider: false ), WideMode]
public float WallThickness { get; set; } = 8.0f;
[Property, Group( "House" ), Title( "Slab Thickness" ), Range( 1, 64, slider: false ), WideMode]
public float SlabThickness { get; set; } = 8.0f;
[Property, Group( "House" ), Range( 0, 256, slider: false ), WideMode]
public float Foundation { get; set; } = 8.0f;
[Property, Group( "Parts" ), Title( "Ground Floor" ), WideMode, Description( "A floor surface inside the walls of the lowest storey." )]
public bool GroundFloor { get; set; } = true;
[Property, Group( "Parts" ), WideMode, Description( "Slabs on top of each storey." )]
public bool Ceilings { get; set; } = true;
[Property, Group( "Parts" ), Title( "Roof On Top" ), WideMode]
public bool Roof { get; set; } = true;
[Property, Group( "Sides" ), Title( "Front Windows" ), WideMode]
public bool WindowsFront { get; set; } = true;
[Property, Group( "Sides" ), Title( "Right Windows" ), WideMode]
public bool WindowsRight { get; set; } = true;
[Property, Group( "Sides" ), Title( "Back Windows" ), WideMode]
public bool WindowsBack { get; set; } = true;
[Property, Group( "Sides" ), Title( "Left Windows" ), WideMode]
public bool WindowsLeft { get; set; } = true;
[Property, Group( "Sides" ), Title( "Door Side" ), WideMode]
public HouseSide DoorSide { get; set; } = HouseSide.Front;
[Property, Group( "Windows" ), Title( "Window Spacing" ), Range( 32, 1024, slider: false ), WideMode]
public float WindowSpacing { get; set; } = 112.0f;
[Property, Group( "Windows" ), Title( "Window Width" ), Range( 8, 512, slider: false ), WideMode]
public float WindowWidth { get; set; } = 48.0f;
[Property, Group( "Windows" ), Title( "Window Height" ), Range( 8, 512, slider: false ), WideMode]
public float WindowHeight { get; set; } = 56.0f;
[Property, Group( "Windows" ), Title( "Sill Height" ), Range( 1, 512, slider: false ), WideMode]
public float SillHeight { get; set; } = 36.0f;
[Property, Group( "Windows" ), Title( "Frame Style" ), WideMode]
public FrameStyle FrameStyle { get; set; } = FrameStyle.Wooden;
[Property, Group( "Windows" ), Title( "Bar Width" ), Range( 0.5f, 32, slider: false ), WideMode]
public float BarWidth { get; set; } = 3.0f;
[Property, Group( "Windows" ), Title( "Vertical Mullions" ), Range( 0, 8, slider: false ), WideMode]
public int VerticalMullions { get; set; } = 1;
[Property, Group( "Windows" ), Title( "Horizontal Mullions" ), Range( 0, 8, slider: false ), WideMode]
public int HorizontalMullions { get; set; } = 1;
[Property, Group( "Windows" ), Title( "Broken Glass" ), Range( 0, 1 ), WideMode]
public float BrokenGlass { get; set; } = 0.0f;
[Property, Group( "Windows" ), Title( "Profiled Frames" ), WideMode, Description( "Profiled frames with real corner joints, glass seated in them and glazing bars. Off: the plain bars of earlier versions." )]
public bool ProfiledFrames { get; set; }
[Property, ShowIf( nameof( ProfiledFrames ), true ), Group( "Windows" ), Title( "Frame Corners" ), WideMode]
public FrameCorner FrameCorner { get; set; } = FrameCorner.Mitred;
[Property, ShowIf( nameof( ProfiledFrames ), true ), Group( "Windows" ), Title( "Frame Profile" ), WideMode]
public FrameProfile FrameProfile { get; set; } = FrameProfile.Chamfered;
[Property, ShowIf( nameof( ProfiledFrames ), true ), Group( "Windows" ), Title( "Frame Detail" ), WideMode]
public WindowDetail FrameDetail { get; set; } = WindowDetail.Standard;
[Property, ShowIf( nameof( ProfiledFrames ), true ), Group( "Windows" ), Title( "Glazing" ), WideMode]
public Glazing FrameGlazing { get; set; } = Glazing.Double;
[Property, Group( "Door" ), Title( "Door Width" ), Range( 16, 256, slider: false ), WideMode]
public float DoorWidth { get; set; } = 56.0f;
[Property, Group( "Door" ), Title( "Door Height" ), Range( 32, 512, slider: false ), WideMode]
public float DoorHeight { get; set; } = 96.0f;
[Property, Group( "Stairs" ), WideMode]
public bool Stairs { get; set; } = true;
[Property, ShowIf( nameof( Stairs ), true ), Group( "Stairs" ), Title( "Stair Width" ), Range( 16, 256, slider: false ), WideMode]
public float StairWidth { get; set; } = 48.0f;
[Property, ShowIf( nameof( Stairs ), true ), Group( "Stairs" ), Title( "Step Depth" ), Range( 4, 64, slider: false ), WideMode]
public float StepDepth { get; set; } = 10.0f;
[Property, ShowIf( nameof( Stairs ), true ), Group( "Stairs" ), Title( "Broken Steps" ), Range( 0, 1 ), WideMode]
public float BrokenSteps { get; set; } = 0.0f;
[Property, ShowIf( nameof( Roof ), true ), Group( "Roof" ), Title( "Roof Shape" ), WideMode]
public HouseRoof RoofShape { get; set; } = HouseRoof.Gable;
[Property, ShowIf( nameof( Roof ), true ), Group( "Roof" ), Title( "Ridge Height" ), Range( 8, 1024, slider: false ), WideMode]
public float RidgeHeight { get; set; } = 96.0f;
[Property, ShowIf( nameof( Roof ), true ), Group( "Roof" ), Range( 0, 128, slider: false ), WideMode]
public float Overhang { get; set; } = 16.0f;
[Property, ShowIf( nameof( Roof ), true ), Group( "Roof" ), Title( "Roof Thickness" ), Range( 1, 32, slider: false ), WideMode]
public float RoofThickness { get; set; } = 4.0f;
[Property, ShowIf( nameof( Roof ), true ), Group( "Roof" ), Title( "Rafter Spacing" ), Range( 8, 256, slider: false ), WideMode]
public float RafterSpacing { get; set; } = 48.0f;
[Property, ShowIf( nameof( Roof ), true ), Group( "Roof" ), WideMode]
public bool Gutters { get; set; } = true;
[Property, ShowIf( nameof( Roof ), true ), Group( "Roof" ), Title( "Ridge Cap" ), WideMode]
public bool RidgeCap { get; set; } = true;
[Property, ShowIf( nameof( Roof ), true ), Group( "Roof" ), WideMode]
public bool Fascia { get; set; } = false;
[Property, Group( "Materials" ), Title( "Walls" ), ResourceType( "vmat" ), WideMode]
public string WallMaterial { get; set; }
[Property, Group( "Materials" ), Title( "Trim" ), ResourceType( "vmat" ), WideMode]
public string TrimMaterial { get; set; }
[Property, Group( "Materials" ), Title( "Floors" ), ResourceType( "vmat" ), WideMode]
public string FloorMaterial { get; set; }
[Property, ShowIf( nameof( Roof ), true ), Group( "Materials" ), Title( "Roof" ), ResourceType( "vmat" ), WideMode]
public string RoofMaterial { get; set; }
[Property, Group( "Materials" ), Title( "Glass" ), ResourceType( "vmat" ), WideMode]
public string GlassMaterial { get; set; }
[Property, Group( "Windows" ), WideMode, Description( "Two leaves beside every window, open flat against the wall." )]
public HouseShutters Shutters { get; set; } = HouseShutters.None;
[Property, Group( "Windows" ), Title( "Window Boxes" ), WideMode, Description( "A flower box under every window." )]
public bool WindowBoxes { get; set; }
[Property, Group( "Door" ), Title( "Door Canopy" ), WideMode, Description( "A small roof over the door." )]
public bool DoorCanopy { get; set; }
[Property, Group( "Door" ), Title( "Door Steps" ), WideMode, Description( "Steps from the ground up to the door, as many as the foundation needs." )]
public bool DoorSteps { get; set; }
}
/// <summary>
/// A whole house, put together only from the requests of the other generators.
/// It decides where things go, what they look like is theirs to decide.
/// </summary>
public sealed class HouseGenerator : IPartGenerator
{
const string FloorCategory = "floor";
const string DoorCategory = "door";
static readonly string[] SideNames = ["front", "right", "back", "left"];
public string Id => "house";
public string Title => "House";
public Type SettingsType => typeof( HouseSettings );
public PartPlan Generate( GeneratorSettings settings, GenContext context )
{
var s = (HouseSettings)settings;
var root = context.Group( "root", "House", "Building", "building" );
var floorMaterial = context.Material( s.FloorMaterial );
root.Add( context.Solid( "foundation", "Foundation", "Foundation", FloorCategory, floorMaterial,
kit => kit.Box( new Vector3( -2.0f, -2.0f, 0.0f ), new Vector3( s.Width + 2.0f, s.Depth + 2.0f, s.Foundation ) ) ) );
if ( s.GroundFloor )
{
var t = s.WallThickness;
root.Add( context.Solid( "ground_floor", "Ground Floor", "FloorSurface", FloorCategory, floorMaterial,
kit => kit.Box( new Vector3( t, t, s.Foundation ), new Vector3( s.Width - t, s.Depth - t, s.Foundation + 1.0f ) ) ) );
}
var stairRise = s.StoreyHeight + s.SlabThickness;
var steps = Math.Max( (int)MathF.Round( stairRise / 7.5f ), 2 );
var run = MathF.Min( steps * s.StepDepth, s.Width - s.WallThickness * 2.0f - 8.0f );
var stairX = s.Width - s.WallThickness - run - 4.0f;
var stairY = s.Depth - s.WallThickness - s.StairWidth * 0.5f - 2.0f;
for ( var storey = 0; storey < s.Storeys; storey++ )
{
var id = $"floor{storey}";
var height = s.Foundation + storey * stairRise;
var floor = root.Add( context.Group( id, $"Floor {storey + 1}", "Floor", "building" ).At( Vector3.Up * height ) );
var hasStairs = s.Stairs && storey < s.Storeys - 1;
for ( var side = 0; side < SideNames.Length; side++ )
floor.Add( BuildSide( s, context, id, side, storey ) );
var hole = hasStairs
? new Rect3( stairX - 2.0f, stairX + run + 2.0f, s.Depth - s.WallThickness - s.StairWidth - 4.0f, s.Depth - s.WallThickness )
: default( Rect3? );
if ( s.Ceilings )
floor.Add( BuildSlab( s, context, $"{id}/ceiling", floorMaterial, hole ) );
if ( !hasStairs ) continue;
var stairs = StairGenerator.Generate( new StairRequest( $"{id}/stairs", "Stairs", run, s.StairWidth, stairRise, steps, 2.0f,
true, true, 36.0f, s.BrokenSteps, s.Variation, floorMaterial, context.Material( s.TrimMaterial ) ), context );
floor.Add( stairs.At( new Vector3( stairX, stairY, 0.0f ) ) );
}
if ( !s.Roof ) return root;
var top = s.Foundation + s.Storeys * stairRise;
root.Add( RoofGenerator.Generate( new RoofRequest( "roof", "Roof", s.Width, s.Depth, s.RidgeHeight, s.Overhang, s.RoofThickness,
s.RoofShape, s.RafterSpacing, s.Gutters, s.Variation, context.Material( s.RoofMaterial ), context.Material( s.TrimMaterial ), s.RidgeCap, s.Fascia ), context ).At( Vector3.Up * top ) );
return root;
}
readonly record struct Rect3( float X0, float X1, float Y0, float Y1 );
/// <summary>
/// Shutters and a flower box on the outer face of the wall, beside and under a window. They touch
/// the wall only; a leaf that would reach the door or past the wall end is left out.
/// </summary>
static void WindowDetails( HouseSettings s, GenContext context, PartPlan group, string id, float center, float width, float slot, float length, float half, float door, Material trim )
{
var outside = -half;
if ( s.Shutters != HouseShutters.None )
{
var leaf = MathF.Min( width * 0.5f, (slot - width) * 0.5f - 2.0f );
if ( leaf >= 6.0f )
{
foreach ( var (x0, x1, name) in new[] { (center - width * 0.5f - leaf, center - width * 0.5f, "left"), (center + width * 0.5f, center + width * 0.5f + leaf, "right") } )
{
if ( x0 < 2.0f || x1 > length - 2.0f )
continue;
if ( door >= 0 && x1 > door - s.DoorWidth * 0.5f - 2.0f && x0 < door + s.DoorWidth * 0.5f + 2.0f )
continue;
var (a, b) = (x0, x1);
group.Add( context.Solid( $"{id}/shutter_{name}", "Shutter", "Shutter", "building", trim, kit =>
{
var z0 = s.SillHeight;
var z1 = s.SillHeight + s.WindowHeight;
if ( s.Shutters == HouseShutters.Panel )
{
kit.Box( new Vector3( a, outside - 1.5f, z0 ), new Vector3( b, outside, z1 ) );
return;
}
// A frame round the leaf and slats across it, the slats a little back. The slats sit
// flush on the frame; lifting flush faces apart would push the leaf into the wall.
kit.LiftFlushFaces = false;
const float rim = 1.5f;
kit.Box( new Vector3( a, outside - 1.5f, z0 ), new Vector3( b, outside, z0 + rim ) );
kit.Box( new Vector3( a, outside - 1.5f, z1 - rim ), new Vector3( b, outside, z1 ) );
kit.Box( new Vector3( a, outside - 1.5f, z0 + rim ), new Vector3( a + rim, outside, z1 - rim ) );
kit.Box( new Vector3( b - rim, outside - 1.5f, z0 + rim ), new Vector3( b, outside, z1 - rim ) );
var slats = Math.Max( 2, (int)MathF.Floor( (z1 - z0 - rim * 2) / 4.0f ) );
var step = (z1 - z0 - rim * 2) / slats;
for ( var k = 0; k < slats; k++ )
{
var bottom = z0 + rim + step * k + step * 0.2f;
kit.Box( new Vector3( a + rim, outside - 1.0f, bottom ), new Vector3( b - rim, outside, bottom + step * 0.6f ) );
}
} ) );
}
}
}
// The window sill hangs 2 below the opening; box and plants stay under it.
var sillBottom = s.SillHeight - 2.0f;
if ( s.WindowBoxes && sillBottom > 16.0f )
{
var top = sillBottom - 3.5f;
group.Add( context.Solid( $"{id}/flower_box", "Window Box", "WindowBox", "building", trim, kit =>
{
kit.Box( new Vector3( center - width * 0.5f, outside - 8.0f, top - 8.0f ), new Vector3( center + width * 0.5f, outside, top ) );
// Plants: low mounds along the box.
var count = Math.Max( 2, (int)MathF.Floor( width / 10.0f ) );
for ( var k = 0; k < count; k++ )
{
var x = center - width * 0.5f + width * (k + 0.5f) / count;
kit.Lathe( new Vector3( x, outside - 4.0f, top ), [new( 0, 0 ), new( 3.5f, 0 ), new( 3.0f, 1.5f ), new( 1.5f, 2.6f ), new( 0, 3.0f )], 8 );
}
} ) );
}
}
static PartPlan BuildSide( HouseSettings s, GenContext context, string floorId, int side, int storey )
{
var t = s.WallThickness;
var half = t * 0.5f;
// Walls run counter clockwise with the inside on their left. The short walls fit between the long ones.
(Vector3 Corner, float Yaw, float Length)[] sides =
[
(new Vector3( 0.0f, half, 0.0f ), 0.0f, s.Width),
(new Vector3( s.Width - half, t, 0.0f ), 90.0f, s.Depth - t * 2.0f),
(new Vector3( s.Width, s.Depth - half, 0.0f ), 180.0f, s.Width),
(new Vector3( half, s.Depth - t, 0.0f ), 270.0f, s.Depth - t * 2.0f)
];
var (corner, yaw, length) = sides[side];
var name = SideNames[side];
var id = $"{floorId}/{name}";
var label = char.ToUpperInvariant( name[0] ) + name[1..];
var group = context.Group( id, $"{label} Side", "Side", "building" ).At( corner, yaw );
var trim = context.Material( s.TrimMaterial );
var glass = context.Material( s.GlassMaterial );
var openings = new List<WallOpening>();
var hasDoor = storey == 0 && side == (int)s.DoorSide;
var hasWindows = side switch
{
0 => s.WindowsFront,
1 => s.WindowsRight,
2 => s.WindowsBack,
_ => s.WindowsLeft
};
var slots = hasWindows ? Math.Max( (int)MathF.Floor( length / MathF.Max( s.WindowSpacing, 32.0f ) ), 1 ) : 0;
var width = slots > 0 ? MathF.Min( s.WindowWidth, length / slots - 8.0f ) : 0.0f;
var windowIndex = 0;
if ( hasDoor )
{
var center = length * 0.5f;
openings.Add( new WallOpening( center - s.DoorWidth * 0.5f, center + s.DoorWidth * 0.5f, 0.0f, s.DoorHeight ) );
group.Add( DoorGenerator.Generate( new DoorRequest( $"{id}/door", "Door", s.DoorWidth, s.DoorHeight, t + 2.0f, s.BarWidth, trim, trim ), context )
.At( new Vector3( center, 0.0f, 0.0f ) ) );
var outside = -half;
if ( s.DoorCanopy )
{
// Above the door frame, below the next storey.
var z = MathF.Min( s.DoorHeight + 6.0f, s.StoreyHeight - 4.0f );
group.Add( context.Solid( $"{id}/canopy", "Door Canopy", "DoorCanopy", "building", trim,
kit => kit.Box( new Vector3( center - s.DoorWidth * 0.5f - 8.0f, outside - 24.0f, z ), new Vector3( center + s.DoorWidth * 0.5f + 8.0f, outside, z + 3.0f ) ) ) );
}
if ( s.DoorSteps && s.Foundation > 0.5f )
{
// From the floor down to the ground, in front of the foundation, which stands 2 out.
var count = Math.Max( 1, (int)MathF.Ceiling( s.Foundation / 8.0f ) );
var rise = s.Foundation / count;
var front = outside - 2.0f;
group.Add( context.Solid( $"{id}/steps", "Door Steps", "DoorSteps", "building", trim, kit =>
{
for ( var k = 0; k < count; k++ )
{
var top = -rise * k;
kit.Box( new Vector3( center - s.DoorWidth * 0.5f - 4.0f, front - 10.0f * (k + 1), -s.Foundation ), new Vector3( center + s.DoorWidth * 0.5f + 4.0f, front - 10.0f * k, top ) );
}
} ) );
}
}
for ( var slot = 0; slot < slots; slot++ )
{
var center = length * (slot + 0.5f) / slots;
// The door takes the place of any window it would run into.
if ( hasDoor && MathF.Abs( center - length * 0.5f ) < (s.DoorWidth + width) * 0.5f + 8.0f ) continue;
openings.Add( new WallOpening( center - width * 0.5f, center + width * 0.5f, s.SillHeight, s.SillHeight + s.WindowHeight ) );
var window = FrameGenerator.Generate( new FrameRequest( $"{id}/window{windowIndex}", $"Window {windowIndex + 1}",
width, s.WindowHeight, t + 2.0f, s.BarWidth, s.FrameStyle, s.VerticalMullions, s.HorizontalMullions,
true, true, s.BrokenGlass, s.Variation, trim, glass, FrameGenerator.Spec( s ) ), context );
group.Add( window.At( new Vector3( center, 0.0f, s.SillHeight ) ) );
WindowDetails( s, context, group, $"{id}/window{windowIndex}", center, width, length / slots, length, half, hasDoor ? length * 0.5f : -1.0f, trim );
windowIndex++;
}
// Without a ceiling the wall has to reach up to the next storey itself.
var height = s.Ceilings ? s.StoreyHeight : s.StoreyHeight + s.SlabThickness;
group.Add( WallGenerator.Generate( new WallRequest( $"{id}/wall", "Wall", length, height, t, openings, s.Variation, context.Material( s.WallMaterial ) ), context ) );
return group;
}
/// <summary>
/// The slab on top of a storey, in up to four panels around the stair opening.
/// </summary>
static PartPlan BuildSlab( HouseSettings s, GenContext context, string id, Material material, Rect3? hole )
{
var group = context.Group( id, "Ceiling", "Slab", FloorCategory );
var z0 = s.StoreyHeight;
var z1 = z0 + s.SlabThickness;
void Panel( string name, string label, float x0, float x1, float y0, float y1 )
{
if ( x1 - x0 < 0.01f || y1 - y0 < 0.01f ) return;
group.Add( context.Solid( $"{id}/{name}", label, "SlabPanel", FloorCategory, material,
kit => kit.Box( new Vector3( x0, y0, z0 ), new Vector3( x1, y1, z1 ) ) ) );
}
if ( hole is not Rect3 h )
{
Panel( "panel", "Slab", 0.0f, s.Width, 0.0f, s.Depth );
return group;
}
Panel( "front", "Front Slab", 0.0f, s.Width, 0.0f, h.Y0 );
Panel( "back", "Back Slab", 0.0f, s.Width, h.Y1, s.Depth );
Panel( "left", "Left Slab", 0.0f, h.X0, h.Y0, h.Y1 );
Panel( "right", "Right Slab", h.X1, s.Width, h.Y0, h.Y1 );
return group;
}
}