editor/AdvancedMappingTools/Procedural/WallExpansion.cs

Editor-side procedural geometry for a wall tool. Holds the plinth and opening-grid settings on ConnectedWallSettings/WallEditor, and AddPlinth builds the base band around each wall section, splitting it into height bands at opening sills and skipping the gaps where openings sit.

namespace AdvancedMappingTools;
public sealed partial class ConnectedWallSettings
{
	[Property, Group( "Opening Grid" ), WideMode]
	public bool OpeningGrid { get; set; }

	[Property, Group( "Opening Grid" ), Range( 1, 16, slider: false ), WideMode]
	public int OpeningRows { get; set; } = 2;

	[Property, Group( "Opening Grid" ), Range( 8, 1024, slider: false ), WideMode]
	public float OpeningRowSpacing { get; set; } = 128;

	[Property, Group( "Opening Grid" ), WideMode]
	public WallOpeningKind AutomaticOpeningKind { get; set; } = WallOpeningKind.Window;

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

	[Property, Group( "Base" ), Range( 1, 256, slider: false ), WideMode]
	public float PlinthHeight { get; set; } = 12;

	[Property, Group( "Base" ), Range( 0, 64, slider: false ), WideMode]
	public float PlinthProjection { get; set; } = 2;

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

public sealed partial class WallEditor
{
	[Property, Group( "Connection" ), WideMode]
	public bool AdvancedCorners { get; set; }

	[Property, Group( "Connection" ), WideMode]
	public WallJoinStyle CornerProfile { get; set; } = WallJoinStyle.Miter;

	[Property, Group( "Opening Grid" ), WideMode]
	public bool OpeningGrid { get; set; }

	[Property, Group( "Opening Grid" ), Range( 1, 32, slider: false ), WideMode]
	public int OpeningColumns { get; set; } = 3;

	[Property, Group( "Opening Grid" ), Range( 1, 16, slider: false ), WideMode]
	public int OpeningRows { get; set; } = 2;

	[Property, Group( "Opening Grid" ), Range( 8, 1024, slider: false ), WideMode]
	public float OpeningRowSpacing { get; set; } = 128;

	[Property, Group( "Opening Grid" ), WideMode]
	public WallOpeningKind OpeningKind { get; set; } = WallOpeningKind.Window;

	[Property, Group( "Opening Grid" ), Range( 4, 1024, slider: false ), WideMode]
	public float GridOpeningWidth { get; set; } = 48;

	[Property, Group( "Opening Grid" ), Range( 4, 1024, slider: false ), WideMode]
	public float GridOpeningHeight { get; set; } = 64;

	[Property, Group( "Opening Grid" ), Range( 0, 1024, slider: false ), WideMode]
	public float GridSill { get; set; } = 32;
}

public sealed partial class ConnectedWallGenerator
{
	void AddPlinth( PartPlan root, ConnectedWallSettings s, GenContext context, PathTrack track, List<WallSection> sections, float z0 )
	{
		var shape = new WallShape( s, track, Math.Clamp( s.PlinthProjection, 0, 64 ) );
		var height = MathF.Min( Math.Clamp( s.PlinthHeight, 1, 256 ), s.Height );
		for ( var sectionIndex = 0; sectionIndex < sections.Count; sectionIndex++ )
		{
			var section = sections[sectionIndex];
			var openings = OpeningsIn( s, section ).Where( o => o.Sill < height ).ToArray();
			var levels = new[]
			{
				0f,
				height
			}.Concat( openings.SelectMany( o => new[] { Math.Clamp( o.Sill, 0, height ), Math.Clamp( o.Sill + o.Height, 0, height ) } ) ).Distinct().OrderBy( v => v ).ToArray();
			for ( var band = 0; band + 1 < levels.Length; band++ )
			{
				var low = levels[band];
				var high = levels[band + 1];
				var middle = (low + high) * .5f;
				var cursor = section.From;
				var piece = 0;
				void Fill( float from, float to )
				{
					if ( to - from < .05f )
						return;
					foreach ( var(a, b)in shape.Pieces( from, to ) )
					{
						var id = $"plinth{sectionIndex}_{band}_{piece++}";
						root.Add( context.Solid( id, "Wall Plinth", "WallPlinth", WallCategory, context.Material( s.PlinthMaterial ?? s.Material ), kit => kit.Prism( shape.Outline( new WallSection { From = section.From, To = section.To }, a, b ), z0 + low, z0 + high ) ) );
					}
				}

				foreach ( var opening in openings.Where( o => o.Sill < middle && o.Sill + o.Height > middle ).OrderBy( o => o.Along ) )
				{
					// The same gap as the wall leaves, so the wall above the plinth meets it everywhere.
					var from = Math.Clamp( MathF.Max( opening.Along - opening.Width * .5f, section.From + PierClearance ), section.From, section.To );
					var to = Math.Clamp( MathF.Min( opening.Along + opening.Width * .5f, section.To - PierClearance ), section.From, section.To );
					Fill( cursor, from );
					cursor = MathF.Max( cursor, to );
				}

				Fill( cursor, section.To );
			}
		}
	}
}