editor/AdvancedMappingTools/Procedural/ParkingLotGenerator.cs

Editor procedural generator for parking lots. Builds a lot surface, curbs with a front entrance gap, painted stall rows, wheel stops and street lamps from ParkingLotSettings.

namespace AdvancedMappingTools;

public enum ParkingRows
{
	[Title( "One Row" )] One,
	[Title( "Two Rows" )] Two,
	[Title( "Rows Back To Back" )] BackToBack
}

public sealed class ParkingLotSettings : GeneratorSettings
{
	[System.Text.Json.Serialization.JsonIgnore] public override IReadOnlySet<string> UnusedOptions { get; } = new HashSet<string> { "Variation" };

	/// <summary>
	/// The size of the rectangle, set by the tool.
	/// </summary>
	public float Width { get; set; } = 1024.0f;
	public float Depth { get; set; } = 768.0f;

	[Property, Group( "Lot" ), Range( 0.5f, 64, slider: false ), WideMode]
	public float Thickness { get; set; } = 6.0f;

	[Property, Group( "Lot" ), WideMode]
	public RoadPaving Paving { get; set; } = RoadPaving.Asphalt;

	[Property, Group( "Stalls" ), Title( "Layout" ), WideMode]
	public ParkingRows Rows { get; set; } = ParkingRows.Two;

	[Property, Group( "Stalls" ), Title( "Stall Width" ), Range( 48, 256, slider: false ), WideMode]
	public float StallWidth { get; set; } = 108.0f;

	[Property, Group( "Stalls" ), Title( "Stall Length" ), Range( 96, 512, slider: false ), WideMode]
	public float StallLength { get; set; } = 216.0f;

	[Property, Group( "Stalls" ), Title( "Line Width" ), Range( 1, 16, slider: false ), WideMode]
	public float LineWidth { get; set; } = 4.0f;

	[Property, Group( "Stalls" ), Title( "Wheel Stops" ), WideMode]
	public bool WheelStops { get; set; } = true;

	[Property, Group( "Stalls" ), Title( "Marking Wear" ), Range( 0, 1 ), WideMode]
	public float MarkingWear { get; set; } = 0.0f;

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

	[Property, ShowIf( nameof( Curbs ), true ), Group( "Edge" ), Title( "Curb Width" ), Range( 2, 64, slider: false ), WideMode]
	public float CurbWidth { get; set; } = 8.0f;

	[Property, ShowIf( nameof( Curbs ), true ), Group( "Edge" ), Title( "Curb Height" ), Range( 1, 32, slider: false ), WideMode]
	public float CurbHeight { get; set; } = 6.0f;

	[Property, Group( "Edge" ), Title( "Entrance Width" ), Range( 0, 1024, slider: false ), WideMode, Description( "An opening in the curb along the front. Draw a road up to it. Zero closes the lot." )]
	public float EntranceWidth { get; set; } = 240.0f;

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

	[Property, ShowIf( nameof( Lamps ), true ), Group( "Details" ), Title( "Lamp Spacing" ), Range( 128, 4096, slider: false ), WideMode]
	public float LampSpacing { get; set; } = 640.0f;

	[Property, Group( "Materials" ), Title( "Surface" ), ResourceType( "vmat" ), WideMode, Description( "Empty paints the lot in the color of its paving." )]
	public string SurfaceMaterial { get; set; }

	[Property, ShowIf( nameof( Curbs ), true ), Group( "Materials" ), Title( "Curbs" ), ResourceType( "vmat" ), WideMode]
	public string CurbMaterial { get; set; }

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

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

/// <summary>
/// A parking lot on a rectangle: a surface, a curb around it with an opening for a road, rows
/// of stalls in paint, wheel stops and lamps. The middle of the front edge is the entrance.
/// </summary>
public sealed class ParkingLotGenerator : IPartGenerator
{
	const string LotCategory = "lot";
	const string MarkingCategory = "marking";
	const string DetailCategory = "roaddetail";

	const float LineLift = 0.15f;
	const float LineEmbed = 0.5f;
	const float WheelStopLength = 72.0f;
	const float WheelStopDepth = 8.0f;
	const float WheelStopHeight = 5.0f;
	const float WheelStopSetback = 24.0f;
	const float WearPiece = 48.0f;

	public string Id => "parkinglot";
	public string Title => "Parking Lot";
	public Type SettingsType => typeof( ParkingLotSettings );
	public IReadOnlyList<string> SeedNames => [LotCategory, MarkingCategory, DetailCategory];

	public PartPlan Generate( GeneratorSettings settings, GenContext context )
	{
		var s = (ParkingLotSettings)settings;
		var root = context.Group( "root", "Parking Lot", "Building", LotCategory );
		var w = s.Width * 0.5f;
		var d = s.Depth * 0.5f;

		root.Add( context.Solid( "surface", "Surface", "LotSurface", LotCategory, context.Material( s.SurfaceMaterial ),
			kit => kit.Box( new Vector3( -w, -d, -s.Thickness ), new Vector3( w, d, 0.0f ) ) ).Paint( s.SurfaceMaterial, RoadColors.Of( s.Paving ) ) );

		if ( s.Curbs ) Curbs( root, context, s, w, d );

		var rows = root.Add( context.Group( "stalls", "Stalls", "Stalls", MarkingCategory ) );
		var margin = s.CurbWidth + 8.0f;
		var count = Math.Max( (int)MathF.Floor( (s.Width - margin * 2.0f) / MathF.Max( s.StallWidth, 16.0f ) ), 1 );
		var start = -count * s.StallWidth * 0.5f;

		void Row( string name, string label, float edge, float direction )
		{
			var group = rows.Add( context.Group( $"stalls/{name}", label, "StallRow", MarkingCategory ) );

			group.Add( context.Solid( $"stalls/{name}/lines", "Stall Lines", "StallLine", MarkingCategory, context.Material( s.MarkingMaterial ), kit =>
			{
				for ( var i = 0; i <= count; i++ )
				{
					var x = start + i * s.StallWidth;

					for ( var at = 0.0f; at < s.StallLength - 0.5f; at += WearPiece )
					{
						if ( context.Stream( MarkingCategory, $"stalls/{name}/{i}/{(int)at}" ).Chance( s.MarkingWear ) ) continue;

						var y0 = edge + direction * at;
						var y1 = edge + direction * MathF.Min( at + WearPiece, s.StallLength );

						kit.Box( new Vector3( x - s.LineWidth * 0.5f, MathF.Min( y0, y1 ), -LineEmbed ), new Vector3( x + s.LineWidth * 0.5f, MathF.Max( y0, y1 ), LineLift ) );
					}
				}
			} ).Paint( s.MarkingMaterial, RoadColors.White ) );

			if ( !s.WheelStops ) return;

			for ( var i = 0; i < count; i++ )
			{
				var x = start + (i + 0.5f) * s.StallWidth;
				var y = edge + direction * WheelStopSetback;

				group.Add( context.Solid( $"stalls/{name}/stop{i}", "Wheel Stop", "WheelStop", DetailCategory, context.Material( s.DetailMaterial ), kit =>
					kit.Box( new Vector3( x - WheelStopLength * 0.5f, y - WheelStopDepth * 0.5f, -LineEmbed ), new Vector3( x + WheelStopLength * 0.5f, y + WheelStopDepth * 0.5f, WheelStopHeight ) ) ).Paint( s.DetailMaterial, RoadColors.Curb ) );
			}
		}

		// The back edge is always parked on, the front only as far as the entrance leaves room.
		Row( "back", "Back Row", d - margin, -1.0f );

		if ( s.Rows == ParkingRows.BackToBack && s.Depth > s.StallLength * 2.0f + margin )
			Row( "back_second", "Second Row", d - margin - s.StallLength * 2.0f, 1.0f );

		if ( s.Rows != ParkingRows.One && s.Depth > s.StallLength * 2.0f + margin * 2.0f && s.EntranceWidth <= 0.0f )
			Row( "front", "Front Row", -d + margin, 1.0f );
		else if ( s.Rows == ParkingRows.Two && s.Depth > s.StallLength * 3.0f )
			Row( "middle", "Middle Row", 0.0f, -1.0f );

		if ( s.Lamps ) Lamps( root, context, s, w, d );

		return root;
	}

	static void Curbs( PartPlan root, GenContext context, ParkingLotSettings s, float w, float d )
	{
		var group = root.Add( context.Group( "curbs", "Curbs", "Curbs", LotCategory ) );
		var c = s.CurbWidth;
		var h = s.CurbHeight;

		void Curb( string name, string label, Vector3 mins, Vector3 maxs )
		{
			if ( maxs.x - mins.x < 1.0f || maxs.y - mins.y < 1.0f ) return;

			group.Add( context.Solid( $"curbs/{name}", label, "Curb", LotCategory, context.Material( s.CurbMaterial ), kit => kit.Box( mins, maxs ) ).Paint( s.CurbMaterial, RoadColors.Curb ) );
		}

		var bottom = -s.Thickness;

		Curb( "back", "Back Curb", new Vector3( -w, d, bottom ), new Vector3( w, d + c, h ) );
		Curb( "left", "Left Curb", new Vector3( -w - c, -d - c, bottom ), new Vector3( -w, d + c, h ) );
		Curb( "right", "Right Curb", new Vector3( w, -d - c, bottom ), new Vector3( w + c, d + c, h ) );

		var open = MathF.Min( s.EntranceWidth, s.Width ) * 0.5f;

		if ( open <= 0.0f )
		{
			Curb( "front", "Front Curb", new Vector3( -w, -d - c, bottom ), new Vector3( w, -d, h ) );
			return;
		}

		Curb( "front_left", "Front Curb", new Vector3( -w, -d - c, bottom ), new Vector3( -open, -d, h ) );
		Curb( "front_right", "Front Curb", new Vector3( open, -d - c, bottom ), new Vector3( w, -d, h ) );
	}

	static void Lamps( PartPlan root, GenContext context, ParkingLotSettings s, float w, float d )
	{
		var group = root.Add( context.Group( "lamps", "Lamps", "Lights", DetailCategory ) );
		var generator = new StreetGenerator();
		var step = MathF.Max( s.LampSpacing, 128.0f );
		var count = Math.Max( (int)MathF.Floor( s.Width / step ), 1 );

		for ( var i = 0; i < count; i++ )
		{
			var x = -w + (i + 0.5f) * s.Width / count;
			var settings = new StreetSettings { Kind = StreetProp.DoubleLamp, MainMaterial = s.DetailMaterial, MetalMaterial = s.DetailMaterial, DetailMaterial = s.DetailMaterial };
			var lamp = generator.Inside( settings, context, $"lamps/lamp{i}", "Lamp", DetailCategory ).At( new Vector3( x, d + s.CurbWidth * 0.5f, s.Curbs ? s.CurbHeight : 0.0f ), 90.0f );

			foreach ( var part in lamp.All() )
				part.Paint( s.DetailMaterial, RoadColors.Metal );

			group.Add( lamp );
		}
	}
}