Editor/Pillar/ArchPillarUi.cs

Editor UI helper for pillar parts in the architecture tool. Renders kinds grid, builds sidebar controls for pillar properties (shaft, footing, span), handles retyping a pillar by replacing its part instance and copying fields.

File Access
using System;
using System.Collections.Generic;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// No section PICKER: a pillar's look comes from its TYPE, filtered by building type.
public static class ArchPillarUi
{
	// One kind is the normal case; a single-entry grid still reads as a choice.
	public static void Kinds( ToolSidebarWidget panel, IReadOnlyList<ArchPillarType> offered, string chosen, Action<ArchPillarType> adopt )
	{
		if ( offered.Count == 0 )
		{
			panel.Layout.Add( ArchSidebarLayout.Advice( "No pillar types are authored — add one to Assets/arch/pillars." ) );

			return;
		}

		using var grid = ArchIconGrid.In( panel.AddGroup( offered.Count == 1 ? "Pillar" : "Pillar type" ) );

		foreach ( var type in offered )
		{
			var captured = type;

			grid.Pick( $"{captured.Title} - {captured.Description}", ArchIcons.PillarSlug( captured.Name ), captured.Icon,
				string.Equals( chosen, captured.Name, StringComparison.OrdinalIgnoreCase ), () => adopt( captured ) );
		}
	}

	// Room entry REPLACED, not patched - one field list stops a setting silently going missing.
	public static ArchPillarPart Retype( ArchTool tool, ArchPillarPart pillar, ArchPillarType type )
	{
		if ( tool.Plan.RoomStanding( pillar ) is not { } room )
		{
			return pillar;
		}

		var dressed = type.Standing( pillar.Origin, pillar.BaseHeight, pillar.Height );

		dressed.Id = pillar.Id;
		dressed.Name = pillar.Name;
		dressed.Yaw = pillar.Yaw;
		dressed.CountX = pillar.CountX;
		dressed.CountY = pillar.CountY;
		dressed.Spacing = pillar.Spacing;

		room.Pillars[room.Pillars.IndexOf( pillar )] = dressed;

		tool.Touch( "Retype Pillars" );

		return dressed;
	}

	// The type gates which groups appear - a plain post has no arcade or pad to tune.
	public static void Build( ToolSidebarWidget panel, ArchPillarPart pillar, ArchPillarType type, Action refresh, Action changed )
	{
		Shaft( panel, pillar, type, false, refresh, changed );
		Footing( panel, pillar, type, refresh, changed );
		Span( panel, pillar, type, refresh, changed );
	}

	public static void Sizes( ToolSidebarWidget panel, ArchPillarPart pillar, ArchPillarType type, Action refresh, Action changed )
	{
		var grid = panel.AddGroup( "Grid" );

		grid.Add( ArchPartUi.Integer( "Columns x", pillar.CountX, 3, value => pillar.CountX = value, changed ) );
		grid.Add( ArchPartUi.Integer( "Columns y", pillar.CountY, 2, value => pillar.CountY = value, changed ) );
		grid.Add( ArchPartUi.Number( "Bay x", pillar.Spacing.x, 240f, value => pillar.Spacing = pillar.Spacing.WithX( value ), changed ) );
		grid.Add( ArchPartUi.Number( "Bay y", pillar.Spacing.y, 240f, value => pillar.Spacing = pillar.Spacing.WithY( value ), changed ) );

		Shaft( panel, pillar, type, true, refresh, changed );
		Footing( panel, pillar, type, refresh, changed );
		Span( panel, pillar, type, refresh, changed );
		Spanning( panel, pillar, type, changed );
	}

	static bool Wanted( ArchPillarType type, ArchPillarGroup group ) => type?.Wants( group ) ?? true;

	// Height belongs to a standing pillar - a draft takes it from whatever it lands under.
	static void Shaft( ToolSidebarWidget panel, ArchPillarPart pillar, ArchPillarType type, bool standing, Action refresh, Action changed )
	{
		var section = Wanted( type, ArchPillarGroup.Section );
		var dressed = Wanted( type, ArchPillarGroup.Ends );

		if ( !section && !dressed )
		{
			return;
		}

		var group = panel.AddGroup( standing ? "Shaft" : "Section" );

		if ( section )
		{
			if ( standing )
			{
				group.Add( ArchPartUi.Number( "Height (0 reaches the soffit)", pillar.Height, 0f, value => pillar.Height = value, changed ) );
			}

			group.Add( ArchPartUi.Number( "Width", pillar.Width, 20f, value => pillar.Width = value, changed ) );
			group.Add( ArchPartUi.Number( "Depth", pillar.Depth, 20f, value => pillar.Depth = value, changed ) );
			group.Add( ArchPartUi.Integer( "Sides", pillar.Sides, 0, value => pillar.Sides = Math.Max( 0, value ), changed ) );
		}

		if ( !dressed )
		{
			return;
		}

		using ( var ends = ArchIconGrid.In( group ) )
		{
			ends.Toggle( "Plinth under each column", "opt_plinth", "vertical_align_bottom", pillar.Plinth,
				value => { pillar.Plinth = value; refresh?.Invoke(); changed?.Invoke(); } );

			ends.Toggle( "Capital on top of each column", "opt_capital", "vertical_align_top", pillar.Capital,
				value => { pillar.Capital = value; refresh?.Invoke(); changed?.Invoke(); } );
		}

		if ( pillar.Plinth )
		{
			group.Add( ArchPartUi.Number( "Plinth height", pillar.PlinthHeight, 8f, value => pillar.PlinthHeight = value, changed ) );
			group.Add( ArchPartUi.Number( "Plinth spread", pillar.PlinthOversize, 3f, value => pillar.PlinthOversize = value, changed ) );
		}

		if ( !pillar.Capital )
		{
			return;
		}

		group.Add( ArchPartUi.Number( "Capital height", pillar.CapitalHeight, 8f, value => pillar.CapitalHeight = value, changed ) );
		group.Add( ArchPartUi.Number( "Capital spread", pillar.CapitalOversize, 3f, value => pillar.CapitalOversize = value, changed ) );
	}

	// The pad is what stops a column reading as a post on the ground.
	static void Footing( ToolSidebarWidget panel, ArchPillarPart pillar, ArchPillarType type, Action refresh, Action changed )
	{
		if ( !Wanted( type, ArchPillarGroup.Footing ) )
		{
			return;
		}

		var group = panel.AddGroup( "Footing" );

		using ( var grid = ArchIconGrid.In( group ) )
		{
			foreach ( var value in Enum.GetValues<PillarFooting>() )
			{
				var captured = value;

				grid.Pick( $"Footing: {FootingHint( captured )}", $"footing_{captured}".ToLowerInvariant(), FootingGlyph( captured ),
					pillar.Footing == captured, () => { pillar.Footing = captured; refresh?.Invoke(); changed?.Invoke(); } );
			}
		}

		if ( pillar.Footing == PillarFooting.None )
		{
			return;
		}

		group.Add( ArchPartUi.Number( "Spread", pillar.FootingSpread, 8f, value => pillar.FootingSpread = value, changed ) );
		group.Add( ArchPartUi.Number( "Depth", pillar.FootingHeight, 10f, value => pillar.FootingHeight = value, changed ) );
		group.Add( ArchPartUi.Number( "Buried", pillar.FootingBuried, 4f, value => pillar.FootingBuried = value, changed ) );
	}

	static void Span( ToolSidebarWidget panel, ArchPillarPart pillar, ArchPillarType type, Action refresh, Action changed )
	{
		if ( !Wanted( type, ArchPillarGroup.Span ) )
		{
			return;
		}

		using ( var grid = ArchIconGrid.In( panel.AddGroup( "Span between them" ) ) )
		{
			foreach ( var value in Enum.GetValues<PillarSpan>() )
			{
				var captured = value;

				grid.Pick( $"Span: {ArchSpanUi.Hint( captured )}", $"span_{captured}".ToLowerInvariant(), ArchSpanUi.Glyph( captured ),
					pillar.Span == captured, () => { pillar.Span = captured; refresh?.Invoke(); changed?.Invoke(); } );
			}
		}

		if ( pillar.Span == PillarSpan.None )
		{
			return;
		}

		using var runs = ArchIconGrid.In( panel.AddGroup( "Which way it runs" ) );

		runs.Toggle( "Span along x", "span_along_x", "swap_horiz", pillar.SpanAlongX,
			value => { pillar.SpanAlongX = value; changed?.Invoke(); } );

		runs.Toggle( "Span along y", "span_along_y", "swap_vert", pillar.SpanAlongY,
			value => { pillar.SpanAlongY = value; changed?.Invoke(); } );

		runs.Toggle( "Ring the grid only — no runs through the middle", "span_perimeter", "crop_square", pillar.PerimeterOnly,
			value => { pillar.PerimeterOnly = value; changed?.Invoke(); } );
	}

	static void Spanning( ToolSidebarWidget panel, ArchPillarPart pillar, ArchPillarType type, Action changed )
	{
		if ( pillar.Span == PillarSpan.None || !Wanted( type, ArchPillarGroup.Span ) )
		{
			return;
		}

		var span = panel.AddGroup( pillar.Span.ToString() );

		if ( pillar.Curved )
		{
			span.Add( ArchPartUi.Number( "Rise", pillar.ArchRise, 48f, value => pillar.ArchRise = value, changed ) );

			if ( pillar.Span == PillarSpan.Arch )
			{
				span.Add( ArchPartUi.Number( "Ring", pillar.ArchRing, 10f, value => pillar.ArchRing = value, changed ) );
			}

			span.Add( ArchPartUi.Integer( "Segments", pillar.ArchSegments, 10, value => pillar.ArchSegments = value, changed ) );

			return;
		}

		span.Add( ArchPartUi.Number( "Depth", pillar.SpanDepth, 14f, value => pillar.SpanDepth = value, changed ) );
		span.Add( ArchPartUi.Number( "Height", pillar.SpanHeight, 18f, value => pillar.SpanHeight = value, changed ) );
	}

	static string FootingHint( PillarFooting value ) => value switch
	{
		PillarFooting.Square => "a plain block pad",
		PillarFooting.Bevelled => "a pad tapering up to the shaft, so there is no shelf to stand on",
		PillarFooting.Stepped => "two pads, the lower one wider",
		_ => "none - the column stands straight on its base"
	};

	static string FootingGlyph( PillarFooting value ) => value switch
	{
		PillarFooting.Square => "crop_square",
		PillarFooting.Bevelled => "change_history",
		PillarFooting.Stepped => "stairs",
		_ => "block"
	};

}