editor/AdvancedMappingTools/Procedural/BuildingGenerators.cs

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 ) ) ) );
		}
	}
}