editor/AdvancedMappingTools/Procedural/District/NewYorkBuildingGenerator.cs

Editor procedural part generator for a New York style building. Builds tiered facade masses, deterministic lit and unlit window panes with lintels, cornice bands, roof, entrance, roof units and optional spire, and nests fire escape and water tower child plans.

namespace AdvancedMappingTools;
public enum NewYorkBuildingStyle
{
	Brownstone,
	Midrise,
	SetbackTower
}

/// <summary>Compact exterior façades for dense city districts; not furnished interiors.</summary>
public sealed class NewYorkBuildingSettings : GeneratorSettings
{
	[Property, Group( "Building" ), WideMode]
	public NewYorkBuildingStyle Style { get; set; }

	[Property, Group( "Building" ), Range( 96, 4096, slider: false ), WideMode]
	public float Width { get; set; } = 320;

	[Property, Group( "Building" ), Range( 96, 4096, slider: false ), WideMode]
	public float Depth { get; set; } = 512;

	[Property, Group( "Building Heights" ), Range( 1, 64, slider: false ), WideMode]
	public int Storeys { get; set; } = 8;

	[Property, Group( "Building Heights" ), Title( "Floor Pitch" ), Range( 80, 256, slider: false ), WideMode]
	public float StoreyHeight { get; set; } = 144;

	[Property, Group( "Facade" ), Title( "Window Spacing" ), Range( 64, 192, slider: false ), WideMode]
	public float WindowSpacing { get; set; } = 96;

	[Property, Group( "Facade" ), Title( "Individual Window Frames" ), WideMode]
	public bool WindowFrames { get; set; }

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

	[Property, Group( "Details" ), Title( "Fire Escape" ), WideMode]
	public bool FireEscape { get; set; }

	[Property, Group( "Details" ), Title( "Roof Water Tank" ), WideMode]
	public bool WaterTank { get; set; }

	[Property, Group( "Details" ), WideMode]
	public bool Spire { get; set; }

	[Property, Group( "Materials" ), WideMode]
	public Color WallColor { get; set; } = new( .55f, .42f, .31f );

	[Property, Group( "Materials" ), ResourceType( "vmat" ), WideMode]
	public string WallMaterial { get; set; }

	[Property, Group( "Materials" ), ResourceType( "vmat" ), WideMode]
	public string RoofMaterial { get; set; }

	[Property, Group( "Materials" ), ResourceType( "vmat" ), WideMode]
	public string GlassMaterial { get; set; }

	[Property, ReadOnly, Group( "Building" ), WideMode]
	public string Mode => "Compact exterior building: façades and roof details, no walkable interior rooms.";
}

public sealed class NewYorkBuildingGenerator : IPartGenerator
{
	public string Id => "nyc_building";
	public string Title => "New York Building";
	public Type SettingsType => typeof(NewYorkBuildingSettings);

	public PartPlan Generate( GeneratorSettings settings, GenContext context )
	{
		var s = (NewYorkBuildingSettings)settings;
		var root = context.Group( "root", Title, "Building", "building" );
		var floors = Math.Clamp( s.Storeys, 1, 64 );
		var pitch = Math.Clamp( s.StoreyHeight, 80, 256 );
		var width = MathF.Max( s.Width, 96 );
		var depth = MathF.Max( s.Depth, 96 );
		var height = floors * pitch;
		var tower = s.Style == NewYorkBuildingStyle.SetbackTower;
		var brown = s.Style == NewYorkBuildingStyle.Brownstone;
		var tiers = new List<(int Start, int End, float X, float Y, float W, float D)>();
		if ( tower && s.Setbacks && floors >= 8 )
		{
			var a = (int)(floors * .65f);
			var b = (int)(floors * .85f);
			tiers.Add( (0, a, 0, 0, width, depth) );
			tiers.Add( (a, b, width * .09f, depth * .09f, width * .82f, depth * .82f) );
			tiers.Add( (b, floors, width * .19f, depth * .19f, width * .62f, depth * .62f) );
		}
		else
			tiers.Add( (0, floors, 0, 0, width, depth) );
		var wall = context.Material( s.WallMaterial );
		var roof = context.Material( s.RoofMaterial );
		var glass = context.Material( s.GlassMaterial );
		var trimColor = brown ? new Color( .45f, .33f, .24f ) : tower ? new Color( .57f, .60f, .61f ) : new Color( .67f, .63f, .55f );
		void Part( string id, string kind, Material material, Color color, Action<MeshKit> build )
		{
			var p = context.Solid( id, id, kind, "building", material, build );
			p.Tint = color;
			root.Add( p );
		}

		Part( "walls", "FacadeMass", wall, s.WallColor, kit =>
		{
			foreach ( var t in tiers )
				kit.Box( new Vector3( t.X, t.Y, t.Start * pitch ), new Vector3( t.X + t.W, t.Y + t.D, t.End * pitch ) );
		} );
		void Windows( MeshKit kit, int role )
		{
			var stream = context.Stream( "glass", "windows" );
			foreach ( var t in tiers )
				for ( var floor = t.Start; floor < t.End; floor++ )
					for ( var side = 0; side < 4; side++ )
					{
						var face = Facade.Side( side, t.W, t.D, 0 );
						var frame = new Transform( new Vector3( t.X, t.Y, 0 ) + face.Corner, Rotation.FromYaw( face.Yaw ) );
						var count = Math.Max( 1, (int)(face.Length / MathF.Max( s.WindowSpacing, 64 )) );
						var slot = face.Length / count;
						var ww = MathF.Min( slot * .55f, 56 );
						var bottom = floor * pitch + (floor == 0 && !brown ? 12 : 30);
						var top = floor * pitch + pitch - 24;
						kit.Frame = frame;
						for ( var i = 0; i < count; i++ )
						{
							var x = slot * (i + .5f);
							var lit = stream.Next() < Math.Clamp( s.Variation, 0, 1 ) * .18f;
							var a = x - ww * .5f;
							var b = x + ww * .5f;
							if ( role == 0 && !lit || role == 1 && lit )
								kit.Box( new Vector3( a, -1, bottom ), new Vector3( b, .2f, top ) );
							if ( role == 2 )
							{
								kit.Box( new Vector3( a - 2, -3, bottom - 3 ), new Vector3( b + 2, .2f, bottom ) );
								kit.Box( new Vector3( a - 2, -3, top ), new Vector3( b + 2, .2f, top + 3 ) );
								if ( s.WindowFrames )
								{
									kit.Box( new Vector3( a - 2, -2, bottom ), new Vector3( a, .2f, top ) );
									kit.Box( new Vector3( b, -2, bottom ), new Vector3( b + 2, .2f, top ) );
									kit.Box( new Vector3( x - .8f, -2, bottom ), new Vector3( x + .8f, .2f, top ) );
								}
							}
						}
					}

			kit.Frame = Transform.Zero;
		}

		Part( "windows", "Glass", glass, new Color( .10f, .17f, .22f ), kit => Windows( kit, 0 ) );
		Part( "lit_windows", "Glass", glass, new Color( .62f, .47f, .25f ), kit => Windows( kit, 1 ) );
		Part( "window_trim", "WindowLintels", wall, trimColor, kit => Windows( kit, 2 ) );
		context.SeedUsers.Add( "lit_windows" );
		Part( "bands", "CornicesAndFloorBands", wall, trimColor, kit =>
		{
			foreach ( var t in tiers )
				for ( var f = t.Start + 1; f <= t.End; f++ )
				{
					if ( tower && f < t.End && f % 4 != 0 )
						continue;
					var z = f * pitch - 8;
					var reach = f == t.End ? 6 : 2;
					kit.Box( new Vector3( t.X - reach, t.Y - reach, z ), new Vector3( t.X + t.W + reach, t.Y, z + 6 ) );
					kit.Box( new Vector3( t.X - reach, t.Y + t.D, z ), new Vector3( t.X + t.W + reach, t.Y + t.D + reach, z + 6 ) );
					kit.Box( new Vector3( t.X - reach, t.Y, z ), new Vector3( t.X, t.Y + t.D, z + 6 ) );
					kit.Box( new Vector3( t.X + t.W, t.Y, z ), new Vector3( t.X + t.W + reach, t.Y + t.D, z + 6 ) );
				}
		} );
		Part( "roof", "TerracesAndRoof", roof, new Color( .23f, .25f, .26f ), kit =>
		{
			foreach ( var t in tiers )
				kit.Box( new Vector3( t.X, t.Y, t.End * pitch ), new Vector3( t.X + t.W, t.Y + t.D, t.End * pitch + 4 ) );
		} );
		Part( "entrance", "Entrance", wall, new Color( .15f, .12f, .10f ), kit =>
		{
			var x = width * .5f;
			kit.Box( new Vector3( x - 20, -2, 0 ), new Vector3( x + 20, .2f, 96 ) );
			if ( brown )
				for ( var i = 0; i < 3; i++ )
					kit.Box( new Vector3( x - 26, -24 + i * 8, 0 ), new Vector3( x + 26, -16 + i * 8, (i + 1) * 6 ) );
			else
				kit.Box( new Vector3( x - 40, -24, 98 ), new Vector3( x + 40, 0, 104 ) );
		} );
		var last = tiers[^1];
		Part( "roof_units", "RoofMechanical", roof, new Color( .38f, .4f, .42f ), kit =>
		{
			kit.Box( new Vector3( last.X + last.W * .12f, last.Y + last.D * .15f, height + 4 ), new Vector3( last.X + last.W * .42f, last.Y + last.D * .35f, height + 52 ) );
			kit.Box( new Vector3( last.X + last.W * .6f, last.Y + last.D * .2f, height + 4 ), new Vector3( last.X + last.W * .85f, last.Y + last.D * .4f, height + 28 ) );
		} );
		if ( tower && s.Spire )
			Part( "spire", "Spire", roof, new Color( .6f, .63f, .65f ), kit =>
			{
				var x = width * .5f;
				var y = depth * .5f;
				kit.Box( new Vector3( x - 16, y - 16, height + 4 ), new Vector3( x + 16, y + 16, height + 64 ) );
				kit.Cylinder( new Vector3( x, y, 0 ), 10, 1, height + 64, height + 240, 12 );
			} );
		PartPlan Prefix( PartPlan source, string prefix )
		{
			var p = new PartPlan
			{
				Id = prefix + "/" + source.Id,
				Name = source.Name,
				Kind = source.Kind,
				Category = source.Category,
				Seed = source.Seed,
				UsesSeed = source.UsesSeed,
				Local = source.Local,
				Mesh = source.Mesh,
				Dimensions = source.Dimensions,
				GeometryHash = source.GeometryHash,
				MaterialHash = source.MaterialHash,
				Smoothing = source.Smoothing,
				Tint = source.Tint
			};
			foreach ( var c in source.Children )
				p.Add( Prefix( c, prefix ) );
			return p;
		}

		if ( s.FireEscape && !tower && floors >= 2 )
		{
			var fs = new FireEscapeSettings
			{
				Storeys = Math.Min( floors - 1, 8 ),
				StoreyHeight = pitch,
				PlatformWidth = Math.Clamp( width * .55f, 96, 160 ),
				PlatformDepth = 40,
				Ladder = true,
				RoofLadder = false,
				Variation = 0
			};
			var escape = Prefix( PartService.Plan( new FireEscapeGenerator(), fs ), "escape" );
			escape.Local = new Transform( new Vector3( width * .5f, 0, 0 ), Rotation.FromYaw( 180 ) );
			foreach ( var p in escape.All().Where( p => p.Mesh != null ) )
				p.Tint = new Color( .12f, .13f, .14f );
			root.Add( escape );
		}

		if ( s.WaterTank && !tower && MathF.Min( width, depth ) >= 192 )
		{
			var ts = new WaterTowerSettings
			{
				Diameter = MathF.Min( 96, MathF.Min( width, depth ) * .28f ),
				Height = 32,
				Shape = WaterTankShape.Barrel,
				Roof = WaterTankRoof.Cone,
				Balcony = false,
				Ladder = false,
				Variation = 0
			};
			var tank = Prefix( PartService.Plan( new WaterTowerGenerator(), ts ), "roof_tank" );
			tank.Local = new Transform( new Vector3( width * .7f, depth * .65f, height + 4 ) );
			foreach ( var p in tank.All().Where( p => p.Mesh != null ) )
				p.Tint = (p.Id.StartsWith( "roof_tank/tank" ) || p.Id.StartsWith( "roof_tank/roof" )) ? new Color( .32f, .22f, .14f ) : new Color( .18f, .2f, .21f );
			root.Add( tank );
		}

		return root;
	}
}