Editor-side procedural building generators for an advanced mapping toolkit. Contains the shared Facade helpers (wall side placement, belt bands, parapet with coping, evenly spaced slots) and two generators: ApartmentGenerator builds a multi storey block with walls, windows, frames, entrance, balconies, belt courses and a flat roof with parapet, stair housing, AC units and a water tank; WarehouseGenerator builds a steel hall with roller doors, a side door, cladding ribs, columns, a roof and a loading dock. Each has a matching GeneratorSettings class exposed as properties in the editor.
namespace AdvancedMappingTools;
/// <summary>
/// The four walls of a rectangle. They run counter clockwise with the outside on their right,
/// and the short walls fit between the long ones. What stands in a wall is placed along its +X.
/// </summary>
public static class Facade
{
public const string BuildingCategory = "building";
static readonly string[] Names = ["front", "right", "back", "left"];
public static string Name( int side ) => Names[side];
public static (Vector3 Corner, float Yaw, float Length) Side( int side, float width, float depth, float thickness, bool miter = false )
{
var half = thickness * 0.5f;
if ( miter )
{
return side switch
{
0 => (new Vector3( 0.0f, half, 0.0f ), 0.0f, width),
1 => (new Vector3( width - half, 0.0f, 0.0f ), 90.0f, depth),
2 => (new Vector3( width, depth - half, 0.0f ), 180.0f, width),
_ => (new Vector3( half, depth, 0.0f ), 270.0f, depth)
};
}
return side switch
{
0 => (new Vector3( 0.0f, half, 0.0f ), 0.0f, width),
1 => (new Vector3( width - half, thickness, 0.0f ), 90.0f, depth - thickness * 2.0f),
2 => (new Vector3( width, depth - half, 0.0f ), 180.0f, width),
_ => (new Vector3( half, depth - thickness, 0.0f ), 270.0f, depth - thickness * 2.0f)
};
}
/// <summary>
/// A ring of four strips around a rectangle, standing out from it by <paramref name="reach"/>.
/// </summary>
public static void Band( PartPlan group, GenContext context, string id, string label, string kind, Material material, float width, float depth, float z0, float z1, float reach )
{
void Strip( string name, Vector3 mins, Vector3 maxs ) =>
group.Add( context.Solid( $"{id}_{name}", label, kind, BuildingCategory, material, kit => kit.Box( mins, maxs ) ) );
// Front and back take the corners, so the side strips fit between them without overlapping.
Strip( "front", new( -reach, -reach, z0 ), new( width + reach, 0.0f, z1 ) );
Strip( "back", new( -reach, depth, z0 ), new( width + reach, depth + reach, z1 ) );
Strip( "left", new( -reach, 0.0f, z0 ), new( 0.0f, depth, z1 ) );
Strip( "right", new( width, 0.0f, z0 ), new( width + reach, depth, z1 ) );
}
/// <summary>
/// A low wall around the edge of a flat roof, with a cap on it.
/// </summary>
public static void Parapet( PartPlan group, GenContext context, string id, Material wall, Material trim, float width, float depth, float z, float height, float thickness )
{
void Piece( string name, string label, string kind, Material material, Vector3 mins, Vector3 maxs ) =>
group.Add( context.Solid( $"{id}_{name}", label, kind, BuildingCategory, material, kit => kit.Box( mins, maxs ) ) );
// Front and back run the full width, the sides fit between them.
Piece( "front", "Parapet", "Parapet", wall, new( 0, 0, z ), new( width, thickness, z + height ) );
Piece( "back", "Parapet", "Parapet", wall, new( 0, depth - thickness, z ), new( width, depth, z + height ) );
Piece( "left", "Parapet", "Parapet", wall, new( 0, thickness, z ), new( thickness, depth - thickness, z + height ) );
Piece( "right", "Parapet", "Parapet", wall, new( width - thickness, thickness, z ), new( width, depth - thickness, z + height ) );
Piece( "cap_front", "Coping", "Coping", trim, new( -1, -1, z + height ), new( width + 1, thickness + 1, z + height + 2 ) );
Piece( "cap_back", "Coping", "Coping", trim, new( -1, depth - thickness - 1, z + height ), new( width + 1, depth + 1, z + height + 2 ) );
Piece( "cap_left", "Coping", "Coping", trim, new( -1, thickness + 1, z + height ), new( thickness + 1, depth - thickness - 1, z + height + 2 ) );
Piece( "cap_right", "Coping", "Coping", trim, new( width - thickness - 1, thickness + 1, z + height ), new( width + 1, depth - thickness - 1, z + height + 2 ) );
}
/// <summary>
/// Evenly spaced places along a wall, keeping away from its ends.
/// </summary>
public static List<float> Slots( float length, float spacing )
{
var count = Math.Max( (int)MathF.Floor( length / MathF.Max( spacing, 16.0f ) ), 1 );
return [.. Enumerable.Range( 0, count ).Select( i => length * (i + 0.5f) / count )];
}
}
public sealed class ApartmentSettings : GeneratorSettings, IProfiledFrameSettings
{
public float Width { get; set; } = 576.0f;
public float Depth { get; set; } = 384.0f;
[Property, Group( "Building" ), Range( 1, 64, slider: false ), WideMode]
public int Storeys { get; set; } = 5;
[Property, Group( "Building" ), Title( "Storey Height" ), Range( 64, 512, slider: false ), WideMode]
public float StoreyHeight { get; set; } = 120.0f;
[Property, Group( "Building" ), Title( "Wall Thickness" ), Range( 2, 64, slider: false ), WideMode]
public float WallThickness { get; set; } = 10.0f;
[Property, Group( "Building" ), Title( "Slab Thickness" ), Range( 2, 64, slider: false ), WideMode]
public float SlabThickness { get; set; } = 8.0f;
[Property, Group( "Building" ), Range( 0, 128, slider: false ), WideMode]
public float Foundation { get; set; } = 12.0f;
[Property, Group( "Windows" ), Title( "Bay Spacing" ), Range( 48, 512, slider: false ), WideMode, Description( "One window every this many units." )]
public float BaySpacing { get; set; } = 96.0f;
[Property, Group( "Windows" ), Title( "Window Width" ), Range( 16, 256, slider: false ), WideMode]
public float WindowWidth { get; set; } = 48.0f;
[Property, Group( "Windows" ), Title( "Window Height" ), Range( 16, 256, slider: false ), WideMode]
public float WindowHeight { get; set; } = 60.0f;
[Property, Group( "Windows" ), Title( "Sill Height" ), Range( 0, 128, slider: false ), WideMode]
public float SillHeight { get; set; } = 36.0f;
[Property, Group( "Windows" ), Title( "Frame Style" ), WideMode]
public FrameStyle FrameStyle { get; set; } = FrameStyle.Metal;
[Property, Group( "Windows" ), Title( "Vertical Mullions" ), Range( 0, 8, slider: false ), WideMode]
public int VerticalMullions { 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 window 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( "Entrance" ), Title( "Door Width" ), Range( 32, 256, slider: false ), WideMode]
public float DoorWidth { get; set; } = 72.0f;
[Property, Group( "Entrance" ), Title( "Door Height" ), Range( 64, 256, slider: false ), WideMode]
public float DoorHeight { get; set; } = 96.0f;
[Property, Group( "Entrance" ), WideMode]
public bool Canopy { get; set; } = true;
[Property, ShowIf( nameof( Canopy ), true ), Group( "Entrance" ), Title( "Canopy Depth" ), Range( 16, 256, slider: false ), WideMode]
public float CanopyDepth { get; set; } = 56.0f;
[Property, Group( "Facade" ), Title( "Belt Courses" ), WideMode, Description( "A strip around the building at every floor." )]
public bool BeltCourses { get; set; } = true;
[Property, ShowIf( nameof( BeltCourses ), true ), Group( "Facade" ), Title( "Belt Depth" ), Range( 0.5f, 16, slider: false ), WideMode]
public float BeltDepth { get; set; } = 3.0f;
[Property, Group( "Balconies" ), WideMode]
public bool Balconies { get; set; } = true;
[Property, ShowIf( nameof( Balconies ), true ), Group( "Balconies" ), Title( "Every Nth Bay" ), Range( 1, 8, slider: false ), WideMode]
public int BalconyEvery { get; set; } = 2;
[Property, ShowIf( nameof( Balconies ), true ), Group( "Balconies" ), Title( "Balcony Depth" ), Range( 16, 128, slider: false ), WideMode]
public float BalconyDepth { get; set; } = 40.0f;
[Property, ShowIf( nameof( Balconies ), true ), Group( "Balconies" ), Title( "Balcony Width" ), Range( 32, 256, slider: false ), WideMode]
public float BalconyWidth { get; set; } = 80.0f;
[Property, Group( "Roof" ), WideMode]
public bool Parapet { get; set; } = true;
[Property, ShowIf( nameof( Parapet ), true ), Group( "Roof" ), Title( "Parapet Height" ), Range( 4, 96, slider: false ), WideMode]
public float ParapetHeight { get; set; } = 28.0f;
[Property, Group( "Roof" ), Title( "Stair Housing" ), WideMode, Description( "The small building on the roof at the top of the stairs." )]
public bool StairHousing { get; set; } = true;
[Property, Group( "Roof" ), Title( "Roof Units" ), Range( 0, 16, slider: false ), WideMode, Description( "Air conditioning boxes." )]
public int RoofUnits { get; set; } = 3;
[Property, Group( "Roof" ), Title( "Water Tank" ), WideMode]
public bool WaterTank { get; set; } = true;
[Property, Group( "Materials" ), Title( "Walls" ), ResourceType( "vmat" ), WideMode]
public string WallMaterial { get; set; }
[Property, Group( "Materials" ), Title( "Roof" ), ResourceType( "vmat" ), WideMode]
public string RoofMaterial { 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, Group( "Materials" ), Title( "Glass" ), ResourceType( "vmat" ), WideMode]
public string GlassMaterial { get; set; }
}
/// <summary>
/// A block of flats: storeys of walls with windows, an entrance, balconies on the front
/// and a flat roof with what stands on one.
/// </summary>
public sealed class ApartmentGenerator : IPartGenerator
{
const string FloorCategory = "floor";
const string BalconyCategory = "balcony";
const string RoofCategory = "roof";
public string Id => "apartment";
public string Title => "Apartment Block";
public Type SettingsType => typeof( ApartmentSettings );
public PartPlan Generate( GeneratorSettings settings, GenContext context )
{
var s = (ApartmentSettings)settings;
var root = context.Group( "root", "Apartment Block", "Building", Facade.BuildingCategory );
if ( s.Width < 96.0f || s.Depth < 96.0f )
return root;
var floor = context.Material( s.FloorMaterial );
var trim = context.Material( s.TrimMaterial );
var wall = context.Material( s.WallMaterial );
var level = s.StoreyHeight + s.SlabThickness;
root.Add( context.Solid( "foundation", "Foundation", "Foundation", FloorCategory, floor, kit => kit.Box( new Vector3( -2, -2, 0 ), new Vector3( s.Width + 2, s.Depth + 2, s.Foundation ) ) ) );
for ( var storey = 0; storey < s.Storeys; storey++ )
{
var z = s.Foundation + storey * level;
var id = $"storey{storey}";
var group = root.Add( context.Group( id, $"Storey {storey + 1}", "Floor", Facade.BuildingCategory ).At( Vector3.Up * z ) );
for ( var side = 0; side < 4; side++ )
group.Add( Side( s, context, id, side, storey ) );
group.Add( context.Solid( $"{id}/slab", "Slab", "Slab", FloorCategory, storey + 1 == s.Storeys && !string.IsNullOrEmpty( s.RoofMaterial ) ? context.Material( s.RoofMaterial ) : floor, kit => kit.Box( new Vector3( 0, 0, s.StoreyHeight ), new Vector3( s.Width, s.Depth, level ) ) ) );
if ( s.BeltCourses )
Facade.Band( group, context, $"{id}/belt", "Belt Course", "BeltCourse", trim, s.Width, s.Depth, s.StoreyHeight, level, s.BeltDepth );
if ( s.Balconies && storey + 1 < s.Storeys )
Balconies( s, context, group, id, trim );
}
if ( s.Canopy )
Canopy( s, context, root, trim );
Roof( s, context, root, wall, trim, s.Foundation + s.Storeys * level );
return root;
}
static PartPlan Side( ApartmentSettings s, GenContext context, string storey, int side, int index )
{
var t = s.WallThickness;
var (corner, yaw, length) = Facade.Side( side, s.Width, s.Depth, t, true );
var name = Facade.Name( side );
var id = $"{storey}/{name}";
var group = context.Group( id, $"{char.ToUpperInvariant( name[0] )}{name[1..]} Side", "Side", Facade.BuildingCategory ).At( corner, yaw );
var trim = context.Material( s.TrimMaterial );
var glass = context.Material( s.GlassMaterial );
var openings = new List<WallOpening>();
var hasDoor = index == 0 && side == 0;
var middle = length * 0.5f;
var windows = 0;
var width = MathF.Min( s.WindowWidth, s.BaySpacing - 16.0f );
// A sill as high as the storey leaves no room for a window, so it stays below the top of it.
var sill = MathF.Min( s.SillHeight, s.StoreyHeight - 24.0f );
if ( hasDoor )
{
openings.Add( new WallOpening( middle - s.DoorWidth * 0.5f, middle + s.DoorWidth * 0.5f, 0.0f, s.DoorHeight ) );
group.Add( DoorGenerator.Generate( new DoorRequest( $"{id}/door", "Entrance Door", s.DoorWidth, s.DoorHeight, t + 2.0f, 3.0f, trim, trim ), context )
.At( new Vector3( middle, 0.0f, 0.0f ) ) );
}
foreach ( var center in Facade.Slots( length, s.BaySpacing ) )
{
if ( hasDoor && MathF.Abs( center - middle ) < (s.DoorWidth + width) * 0.5f + 8.0f ) continue;
var margin = t + 4.0f;
if ( center - width * 0.5f < margin || center + width * 0.5f > length - margin ) continue;
var height = MathF.Min( s.WindowHeight, s.StoreyHeight - sill - 4.0f );
openings.Add( new WallOpening( center - width * 0.5f, center + width * 0.5f, sill, sill + height ) );
var window = FrameGenerator.Generate( new FrameRequest( $"{id}/window{windows}", $"Window {windows + 1}", width, height, t + 2.0f, 3.0f,
s.FrameStyle, s.VerticalMullions, 0, true, true, s.BrokenGlass, s.Variation, trim, glass, FrameGenerator.Spec( s ) ), context );
group.Add( window.At( new Vector3( center, 0.0f, sill ) ) );
windows++;
}
group.Add( WallGenerator.Generate( new WallRequest( $"{id}/wall", "Wall", length, s.StoreyHeight, t, openings, s.Variation, context.Material( s.WallMaterial ), true, true ), context ) );
return group;
}
static void Balconies( ApartmentSettings s, GenContext context, PartPlan group, string id, Material trim )
{
var belt = s.BeltCourses ? s.BeltDepth : 0.0f;
var slots = Facade.Slots( s.Width, s.BaySpacing );
for ( var slot = 0; slot < slots.Count; slot += Math.Max( s.BalconyEvery, 1 ) )
{
var name = $"{id}/balcony{slot}";
var balcony = group.Add( context.Group( name, $"Balcony {slot + 1}", "Balcony", BalconyCategory ) );
var x0 = slots[slot] - s.BalconyWidth * 0.5f;
var x1 = slots[slot] + s.BalconyWidth * 0.5f;
var front = -belt - s.BalconyDepth;
var z = s.StoreyHeight;
var top = z + s.SlabThickness;
var rail = top + 36.0f;
if ( x0 < 2.0f || x1 > s.Width - 2.0f ) continue;
void Piece( string part, string label, string kind, Vector3 mins, Vector3 maxs ) =>
balcony.Add( context.Solid( $"{name}/{part}", label, kind, BalconyCategory, trim, kit => kit.Box( mins, maxs ) ) );
Piece( "slab", "Balcony Slab", "BalconySlab", new( x0, front, z ), new( x1, -belt, top ) );
Piece( "post_left", "Post", "RailingPost", new( x0, front, top ), new( x0 + 2, front + 2, rail ) );
Piece( "post_right", "Post", "RailingPost", new( x1 - 2, front, top ), new( x1, front + 2, rail ) );
Piece( "rail_front", "Rail", "Rail", new( x0, front, rail ), new( x1, front + 2, rail + 2 ) );
Piece( "rail_left", "Rail", "Rail", new( x0, front + 2, rail ), new( x0 + 2, 0, rail + 2 ) );
Piece( "rail_right", "Rail", "Rail", new( x1 - 2, front + 2, rail ), new( x1, 0, rail + 2 ) );
for ( var bar = 1; bar * 8.0f < x1 - x0 - 4.0f; bar++ )
{
var at = x0 + bar * 8.0f;
Piece( $"bar{bar}", "Bar", "RailingBar", new( at, front + 0.5f, top ), new( at + 1, front + 1.5f, rail ) );
}
}
}
static void Canopy( ApartmentSettings s, GenContext context, PartPlan root, Material trim )
{
var group = root.Add( context.Group( "canopy", "Canopy", "Canopy", Facade.BuildingCategory ) );
var z0 = s.Foundation + s.DoorHeight + 6.0f;
var half = s.DoorWidth * 0.5f + 24.0f;
var middle = s.Width * 0.5f;
var belt = 0.0f;
group.Add( context.Solid( "canopy/roof", "Canopy Roof", "CanopyRoof", Facade.BuildingCategory, trim,
kit => kit.Box( new Vector3( middle - half, -s.CanopyDepth, z0 ), new Vector3( middle + half, belt, z0 + 6 ) ) ) );
foreach ( var (x, name) in new[] { (middle - half, "left"), (middle + half - 5, "right") } )
{
group.Add( context.Solid( $"canopy/post_{name}", "Canopy Post", "CanopyPost", Facade.BuildingCategory, trim,
kit => kit.Box( new Vector3( x, -s.CanopyDepth, 0 ), new Vector3( x + 5, -s.CanopyDepth + 5, z0 ) ) ) );
}
}
static void Roof( ApartmentSettings s, GenContext context, PartPlan root, Material wall, Material trim, float z )
{
var group = root.Add( context.Group( "roof", "Roof", "RoofAssembly", RoofCategory ) );
if ( s.Parapet )
Facade.Parapet( group, context, "roof/parapet", wall, trim, s.Width, s.Depth, z, s.ParapetHeight, s.WallThickness );
var inside = s.WallThickness + 16.0f;
if ( s.StairHousing )
{
var x = s.Width * 0.5f - 60.0f;
var y = s.Depth - inside - 96.0f;
group.Add( context.Solid( "roof/stair_housing", "Stair Housing", "StairHousing", RoofCategory, wall,
kit => kit.Box( new Vector3( x, y, z ), new Vector3( x + 120, y + 96, z + 96 ) ) ) );
group.Add( context.Solid( "roof/stair_housing_top", "Housing Roof", "HousingRoof", RoofCategory, trim,
kit => kit.Box( new Vector3( x - 3, y - 3, z + 96 ), new Vector3( x + 123, y + 99, z + 100 ) ) ) );
group.Add( context.Solid( "roof/stair_door", "Roof Door", "DoorLeaf", RoofCategory, trim,
kit => kit.Box( new Vector3( x + 40, y - 1, z ), new Vector3( x + 80, y, z + 84 ) ) ) );
}
for ( var i = 0; i < s.RoofUnits; i++ )
{
var x = inside + i * MathF.Min( 64.0f, (s.Width - inside * 2.0f - 48.0f) / Math.Max( s.RoofUnits, 1 ) );
var y = inside;
var name = $"roof/unit{i}";
var unit = group.Add( context.Group( name, $"Roof Unit {i + 1}", "RoofUnit", RoofCategory ) );
unit.Add( context.Solid( $"{name}/body", "Body", "RoofUnitBody", RoofCategory, trim,
kit => kit.Box( new Vector3( x, y, z ), new Vector3( x + 44, y + 32, z + 24 ) ) ) );
unit.Add( context.Solid( $"{name}/fan", "Fan", "RoofUnitFan", RoofCategory, trim,
kit => kit.Cylinder( new Vector3( x + 22, y + 16, 0 ), 12, 12, z + 24, z + 26, 12 ) ) );
}
if ( !s.WaterTank ) return;
var tank = group.Add( context.Group( "roof/tank", "Water Tank", "WaterTank", RoofCategory ) );
var cx = s.Width - inside - 44.0f;
var cy = s.Depth - inside - 44.0f;
foreach ( var (dx, dy, name) in new[] { (-30f, -30f, "a"), (26f, -30f, "b"), (-30f, 26f, "c"), (26f, 26f, "d") } )
{
tank.Add( context.Solid( $"roof/tank/leg_{name}", "Leg", "TankLeg", RoofCategory, trim,
kit => kit.Box( new Vector3( cx + dx, cy + dy, z ), new Vector3( cx + dx + 4, cy + dy + 4, z + 48 ) ) ) );
}
tank.Add( context.Solid( "roof/tank/deck", "Deck", "TankDeck", RoofCategory, trim,
kit => kit.Box( new Vector3( cx - 32, cy - 32, z + 48 ), new Vector3( cx + 32, cy + 32, z + 51 ) ) ) );
tank.Add( context.Solid( "roof/tank/body", "Tank", "TankBody", RoofCategory, trim,
kit => kit.Cylinder( new Vector3( cx, cy, 0 ), 30, 30, z + 51, z + 111, 16 ) ) );
tank.Add( context.Solid( "roof/tank/roof", "Tank Roof", "TankRoof", RoofCategory, trim,
kit => kit.Cylinder( new Vector3( cx, cy, 0 ), 32, 3, z + 111, z + 127, 16 ) ) );
}
}
public sealed class WarehouseSettings : GeneratorSettings
{
public float Width { get; set; } = 768.0f;
public float Depth { get; set; } = 512.0f;
[Property, Group( "Hall" ), Range( 96, 1024, slider: false ), WideMode]
public float Height { get; set; } = 240.0f;
[Property, Group( "Hall" ), Title( "Wall Thickness" ), Range( 2, 32, slider: false ), WideMode]
public float WallThickness { get; set; } = 6.0f;
[Property, Group( "Hall" ), Title( "Roof Rise" ), Range( 8, 512, slider: false ), WideMode]
public float RoofRise { get; set; } = 64.0f;
[Property, Group( "Hall" ), Title( "Roof Shape" ), WideMode, Description( "Any shape the roof toolkit knows, not only a gable." )]
public HouseRoof RoofShape { get; set; } = HouseRoof.Gable;
[Property, Group( "Hall" ), Title( "Bay Spacing" ), Range( 64, 512, slider: false ), WideMode, Description( "Distance between the steel columns." )]
public float BaySpacing { get; set; } = 192.0f;
[Property, Group( "Doors" ), Title( "Roller Doors" ), Range( 0, 8, slider: false ), WideMode]
public int RollerDoors { get; set; } = 2;
[Property, HideIf( nameof( RollerDoors ), 0 ), Group( "Doors" ), Title( "Door Width" ), Range( 64, 512, slider: false ), WideMode]
public float DoorWidth { get; set; } = 144.0f;
[Property, HideIf( nameof( RollerDoors ), 0 ), Group( "Doors" ), Title( "Door Height" ), Range( 64, 512, slider: false ), WideMode]
public float DoorHeight { get; set; } = 156.0f;
[Property, HideIf( nameof( RollerDoors ), 0 ), Group( "Doors" ), Title( "Doors Open" ), Range( 0, 1 ), WideMode, Description( "How far the roller doors are rolled up." )]
public float DoorsOpen { get; set; } = 0.0f;
[Property, Group( "Details" ), Title( "Cladding Ribs" ), WideMode, Description( "Steel ribs up the outside of the walls." )]
public bool CladdingRibs { get; set; } = true;
[Property, ShowIf( nameof( CladdingRibs ), true ), Group( "Details" ), Title( "Rib Spacing" ), Range( 8, 128, slider: false ), WideMode]
public float RibSpacing { get; set; } = 32.0f;
[Property, Group( "Details" ), Title( "Loading Dock" ), WideMode]
public bool LoadingDock { get; set; } = true;
[Property, ShowIf( nameof( LoadingDock ), true ), Group( "Details" ), Title( "Dock Height" ), Range( 16, 96, slider: false ), WideMode]
public float DockHeight { get; set; } = 44.0f;
[Property, ShowIf( nameof( LoadingDock ), true ), Group( "Details" ), Title( "Dock Depth" ), Range( 32, 256, slider: false ), WideMode]
public float DockDepth { get; set; } = 96.0f;
[Property, Group( "Details" ), Title( "Gutters" ), WideMode]
public bool Gutters { get; set; } = true;
[Property, Group( "Materials" ), Title( "Walls" ), ResourceType( "vmat" ), WideMode]
public string WallMaterial { get; set; }
[Property, Group( "Materials" ), Title( "Steel" ), ResourceType( "vmat" ), WideMode]
public string SteelMaterial { get; set; }
[Property, Group( "Materials" ), Title( "Roof" ), ResourceType( "vmat" ), WideMode]
public string RoofMaterial { get; set; }
[Property, Group( "Materials" ), Title( "Concrete" ), ResourceType( "vmat" ), WideMode]
public string ConcreteMaterial { get; set; }
}
/// <summary>
/// A steel hall with roller doors, cladding and a loading dock.
/// </summary>
public sealed class WarehouseGenerator : IPartGenerator
{
const string HallCategory = "hall";
const float ColumnSize = 10.0f;
public string Id => "warehouse";
public string Title => "Warehouse";
public Type SettingsType => typeof( WarehouseSettings );
public PartPlan Generate( GeneratorSettings settings, GenContext context )
{
var s = (WarehouseSettings)settings;
var root = context.Group( "root", "Warehouse", "Building", Facade.BuildingCategory );
if ( s.Width < 128.0f || s.Depth < 128.0f ) return root;
var wall = context.Material( s.WallMaterial );
var steel = context.Material( s.SteelMaterial );
var concrete = context.Material( s.ConcreteMaterial );
var t = s.WallThickness;
root.Add( context.Solid( "slab", "Floor Slab", "Foundation", HallCategory, concrete,
kit => kit.Box( new Vector3( -ColumnSize, -ColumnSize, 0 ), new Vector3( s.Width + ColumnSize, s.Depth + ColumnSize, 4 ) ) ) );
var walls = root.Add( context.Group( "walls", "Walls", "Walls", Facade.BuildingCategory ).At( Vector3.Up * 4.0f ) );
for ( var side = 0; side < 4; side++ )
{
var (corner, yaw, length) = Facade.Side( side, s.Width, s.Depth, t );
var id = Facade.Name( side );
var group = walls.Add( context.Group( id, id, "Side", Facade.BuildingCategory ).At( corner, yaw ) );
var openings = new List<WallOpening>();
if ( side == 0 ) Doors( s, context, group, id, length, openings, steel );
if ( side == 1 )
{
var center = length * 0.25f;
openings.Add( new WallOpening( center - 20.0f, center + 20.0f, 0.0f, 88.0f ) );
group.Add( DoorGenerator.Generate( new DoorRequest( $"{id}/door", "Side Door", 40.0f, 88.0f, t + 2.0f, 2.0f, steel, steel ), context )
.At( new Vector3( center, 0.0f, 0.0f ) ) );
}
group.Add( WallGenerator.Generate( new WallRequest( $"{id}/wall", "Cladding", length, s.Height, t, openings, s.Variation, wall ), context ) );
if ( s.CladdingRibs ) group.Add( Ribs( s, context, id, length, openings, steel ) );
}
Columns( s, context, root, steel );
root.Add( RoofGenerator.Generate( new RoofRequest( "roof", "Roof", s.Width, s.Depth, s.RoofRise, 12.0f, 4.0f, s.RoofShape,
s.BaySpacing * 0.5f, s.Gutters, s.Variation, context.Material( s.RoofMaterial ), steel ), context ).At( Vector3.Up * (4.0f + s.Height) ) );
if ( s.LoadingDock ) Dock( s, context, root, concrete, steel );
return root;
}
static List<float> DoorCenters( WarehouseSettings s, float length )
{
var count = Math.Min( s.RollerDoors, (int)(length / (s.DoorWidth + 32.0f)) );
return [.. Enumerable.Range( 0, Math.Max( count, 0 ) ).Select( i => length * (i + 0.5f) / count )];
}
static void Doors( WarehouseSettings s, GenContext context, PartPlan group, string id, float length, List<WallOpening> openings, Material steel )
{
var height = MathF.Min( s.DoorHeight, s.Height - 8.0f );
var depth = s.WallThickness * 0.5f;
var open = Math.Clamp( s.DoorsOpen, 0.0f, 0.95f );
var index = 0;
foreach ( var center in DoorCenters( s, length ) )
{
var x0 = center - s.DoorWidth * 0.5f;
var x1 = center + s.DoorWidth * 0.5f;
var name = $"{id}/roller{index}";
var door = group.Add( context.Group( name, $"Roller Door {index + 1}", "RollerDoor", HallCategory ) );
openings.Add( new WallOpening( x0, x1, 0.0f, height ) );
door.Add( context.Solid( $"{name}/curtain", "Curtain", "RollerCurtain", HallCategory, steel,
kit => kit.Box( new Vector3( x0 + 3, -1, height * open ), new Vector3( x1 - 3, 1, height ) ) ) );
door.Add( context.Solid( $"{name}/guide_left", "Guide", "RollerGuide", HallCategory, steel,
kit => kit.Box( new Vector3( x0, -depth, 0 ), new Vector3( x0 + 3, depth, height ) ) ) );
door.Add( context.Solid( $"{name}/guide_right", "Guide", "RollerGuide", HallCategory, steel,
kit => kit.Box( new Vector3( x1 - 3, -depth, 0 ), new Vector3( x1, depth, height ) ) ) );
index++;
}
}
static PartPlan Ribs( WarehouseSettings s, GenContext context, string id, float length, List<WallOpening> openings, Material steel )
{
var outside = -s.WallThickness * 0.5f;
return context.Solid( $"{id}/ribs", "Ribs", "CladdingRibs", HallCategory, steel, kit =>
{
kit.BeginPart();
var columns = id is "front" or "back" ? ColumnPlaces( s ) : [];
for ( var x = s.RibSpacing * 0.5f; x < length - 2.0f; x += MathF.Max( s.RibSpacing, 8.0f ) )
{
if ( columns.Any( c => x + 2.0f > c && x < c + ColumnSize ) ) continue;
var blocked = openings.FirstOrDefault( o => x + 2.0f > o.Start && x < o.End );
var from = blocked.End > blocked.Start ? blocked.Head : 0.0f;
if ( from < s.Height - 1.0f )
kit.Box( new Vector3( x, outside - 1.0f, from ), new Vector3( x + 2.0f, outside, s.Height ) );
}
} );
}
static List<float> ColumnPlaces( WarehouseSettings s )
{
var bays = Math.Max( (int)MathF.Round( s.Width / MathF.Max( s.BaySpacing, 64.0f ) ), 1 );
return [.. Enumerable.Range( 0, bays + 1 ).Select( i => Math.Clamp( s.Width * i / bays - ColumnSize * 0.5f, 0.0f, s.Width - ColumnSize ) )];
}
static void Columns( WarehouseSettings s, GenContext context, PartPlan root, Material steel )
{
var group = root.Add( context.Group( "columns", "Columns", "Columns", HallCategory ) );
var places = ColumnPlaces( s );
for ( var i = 0; i < places.Count; i++ )
{
var x = places[i];
foreach ( var (y, name) in new[] { (-ColumnSize, "front"), (s.Depth, "back") } )
{
group.Add( context.Solid( $"columns/{name}{i}", "Column", "SteelColumn", HallCategory, steel,
kit => kit.Box( new Vector3( x, y, 4 ), new Vector3( x + ColumnSize, y + ColumnSize, s.Height + 4 ) ) ) );
}
}
}
static void Dock( WarehouseSettings s, GenContext context, PartPlan root, Material concrete, Material steel )
{
var group = root.Add( context.Group( "dock", "Loading Dock", "LoadingDock", HallCategory ) );
var y0 = -ColumnSize - s.DockDepth;
var height = MathF.Min( s.DockHeight, s.DoorHeight - 16.0f );
group.Add( context.Solid( "dock/platform", "Platform", "DockPlatform", HallCategory, concrete,
kit => kit.Box( new Vector3( 0, y0, 0 ), new Vector3( s.Width, -ColumnSize, height ) ) ) );
for ( var i = 0; i < 6; i++ )
{
var x = s.Width * (i + 0.5f) / 6.0f;
group.Add( context.Solid( $"dock/bumper{i}", "Bumper", "DockBumper", HallCategory, steel,
kit => kit.Box( new Vector3( x - 8, y0 - 3, height - 24 ), new Vector3( x + 8, y0, height - 8 ) ) ) );
}
}
}