editor/AdvancedMappingTools/Procedural/WallGenerator.cs

Procedural wall generator for the editor's Advanced Mapping Tools package. Splits a straight wall into panels (piers, sill and header sections around each opening), builds each panel as a box or a mitred prism, and wraps it in a generator that also exposes WallPartsSettings for length, height, thickness, opening count and sizes.

namespace AdvancedMappingTools;

public readonly record struct WallOpening( float Start, float End, float Sill, float Head );

public sealed record WallRequest(
	string Id, string Name, float Length, float Height, float Thickness,
	IReadOnlyList<WallOpening> Openings, float Variation, Material Material,
	bool MiterStart = false, bool MiterEnd = false );

/// <summary>
/// A straight wall as separate panels: a pier between openings, one panel below
/// and one above each opening. Runs along +X with its middle on the X axis.
/// </summary>
public static class WallGenerator
{
	public const string Category = "wall";

	public static PartPlan Generate( WallRequest request, GenContext context )
	{
		var group = context.Group( request.Id, request.Name, "Wall", Category );
		var cursor = 0.0f;
		var index = 0;

		void Panel( string name, string label, float x0, float x1, float z0, float z1 )
		{
			if ( x1 - x0 < 0.01f || z1 - z0 < 0.01f ) return;

			var id = $"{request.Id}/{name}";
			var stream = context.Stream( Category, id );
			var miterStart = request.MiterStart && x0 <= 0.01f;
			var miterEnd = request.MiterEnd && x1 >= request.Length - 0.01f;

			// Two mitres only meet when both walls are as thick as planned.
			var half = request.Thickness * 0.5f + (miterStart || miterEnd ? 0.0f : request.Variation * stream.Range( -0.25f, 0.25f ));

			group.Add( context.Solid( id, label, "WallPanel", Category, request.Material, kit =>
			{
				if ( !miterStart && !miterEnd )
				{
					kit.Box( new Vector3( x0, -half, z0 ), new Vector3( x1, half, z1 ) );
					return;
				}

				// A mitre: the outer face runs to the end, the inner face stops a wall thickness short.
				var knee = MathF.Min( half * 2.0f, (x1 - x0) * (miterStart && miterEnd ? 0.5f : 1.0f) );
				var shape = new List<Vector3>
				{
					new( x0, -half, 0.0f ),
					new( x1, -half, 0.0f ),
					new( miterEnd ? x1 - knee : x1, half, 0.0f ),
					new( miterStart ? x0 + knee : x0, half, 0.0f )
				};

				kit.Prism( shape, z0, z1 );
			} ) );
		}

		foreach ( var opening in request.Openings.OrderBy( x => x.Start ) )
		{
			Panel( $"pier{index}", $"Pier {index + 1}", cursor, opening.Start, 0.0f, request.Height );
			Panel( $"below{index}", $"Below Opening {index + 1}", opening.Start, opening.End, 0.0f, opening.Sill );
			Panel( $"above{index}", $"Above Opening {index + 1}", opening.Start, opening.End, opening.Head, request.Height );

			cursor = opening.End;
			index++;
		}

		Panel( $"pier{index}", $"Pier {index + 1}", cursor, request.Length, 0.0f, request.Height );

		return group;
	}
}

public sealed class WallPartsSettings : GeneratorSettings
{
	public float Length { get; set; } = 256.0f;

	[Property, Group( "Wall" ), Range( 8, 4096, slider: false ), WideMode]
	public float Height { get; set; } = 128.0f;

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

	[Property, Group( "Wall" ), Range( 0, 32, slider: false ), WideMode]
	public int Openings { get; set; } = 1;

	[Property, HideIf( nameof( Openings ), 0 ), Group( "Wall" ), Title( "Opening Width" ), Range( 4, 1024, slider: false ), WideMode]
	public float OpeningWidth { get; set; } = 48.0f;

	[Property, HideIf( nameof( Openings ), 0 ), Group( "Wall" ), Title( "Sill Height" ), Range( 0, 1024, slider: false ), WideMode]
	public float SillHeight { get; set; } = 36.0f;

	[Property, HideIf( nameof( Openings ), 0 ), Group( "Wall" ), Title( "Opening Height" ), Range( 4, 1024, slider: false ), WideMode]
	public float OpeningHeight { get; set; } = 56.0f;

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

public sealed class WallPartsGenerator : IPartGenerator
{
	public string Id => "wall";
	public string Title => "Wall";
	public Type SettingsType => typeof( WallPartsSettings );

	public PartPlan Generate( GeneratorSettings settings, GenContext context )
	{
		var s = (WallPartsSettings)settings;
		var root = context.Group( "root", "Wall", "Building", "wall" );
		var openings = new List<WallOpening>();

		for ( var i = 0; i < s.Openings; i++ )
		{
			var center = s.Length * (i + 0.5f) / s.Openings;
			openings.Add( new WallOpening( center - s.OpeningWidth * 0.5f, center + s.OpeningWidth * 0.5f, s.SillHeight, s.SillHeight + s.OpeningHeight ) );
		}

		root.Add( WallGenerator.Generate( new WallRequest( "wall", "Wall", s.Length, s.Height, s.Thickness, openings, s.Variation, context.Material( s.Material ) ), context ) );

		return root;
	}
}