Editor/Tool/Subtools/ArchBarrierUi.cs

Editor UI helper that builds the sidebar controls for configuring architectural barrier specs. It renders style, ground, bay, gate, slab, electric, infill and disrepair controls and updates the ArchBarrierSpec accordingly via callbacks.

Reflection
using System;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

public static class ArchBarrierUi
{
	public static void Build( ToolSidebarWidget panel, ArchBarrierSpec spec, Action refresh, Action changed = null )
	{
		Styles( panel, spec, refresh, changed );
		Bays( panel, spec, changed );

		if ( spec.Style == BarrierStyle.Retaining )
		{
			Disrepair( panel, spec, changed );

			return;
		}

		Gate( panel, spec, refresh, changed );

		if ( spec.Style == BarrierStyle.PrecastSlab )
		{
			Slabs( panel, spec, changed );
			Electric( panel, spec, refresh, changed );
		}
		else
		{
			Infill( panel, spec, changed );
		}

		Disrepair( panel, spec, changed );
	}

	static void Styles( ToolSidebarWidget panel, ArchBarrierSpec spec, Action refresh, Action changed )
	{
		ArchSidebarSection.Show( panel, "Barrier.style", "Barrier", group =>
		{
			using ( var grid = ArchIconGrid.In( group ) )
			{
				foreach ( var style in Enum.GetValues<BarrierStyle>() )
				{
					var captured = style;

					grid.Pick( $"Style: {captured}", $"barrier_{captured}".ToLowerInvariant(), Icon( captured ),
						spec.Style == captured, () => { spec.Style = captured; refresh?.Invoke(); changed?.Invoke(); } );
				}
			}

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

					grid.Pick( $"Ground: {captured}", $"ground_{captured}".ToLowerInvariant(), GroundIcon( captured ),
						spec.Ground == captured, () => { spec.Ground = captured; changed?.Invoke(); } );
				}
			}
		} );
	}

	static void Bays( ToolSidebarWidget panel, ArchBarrierSpec spec, Action changed )
	{
		var retaining = spec.Style == BarrierStyle.Retaining;

		ArchSidebarSection.Show( panel, "Barrier.bays", "Bays", group =>
		{
			// A retaining wall's head is the grade it holds and its section is battered off the kit, so a typed
			// height and a post size would only fight the ground.
			if ( !retaining )
			{
				group.Add( ArchPartUi.Number( "Height", spec.Height, 40f, value => spec.Height = value, changed ) );
				group.Add( ArchPartUi.Number( "Post size", spec.PostSize, 5f, value => spec.PostSize = value, changed ) );
			}

			group.Add( ArchPartUi.Number( retaining ? "Joint pitch" : "Panel length", spec.PanelLength, 96f, value => spec.PanelLength = value, changed ) );

			group.Add( ArchPartUi.Check( "Divide bays evenly", spec.EvenBays, value => spec.EvenBays = value, changed ) );
		} );
	}

	// A zero-width gate is no gate, so the width IS the enable - the two numbers only mean anything together.
	static void Gate( ToolSidebarWidget panel, ArchBarrierSpec spec, Action refresh, Action changed )
	{
		ArchSidebarSection.Show( panel, "Barrier.gate", "Gate", group =>
		{
			using ( var grid = ArchIconGrid.In( group ) )
			{
				grid.Toggle( "Leave a gate in the run", "opt_gate", "meeting_room", spec.GateWidth > 0f, value =>
				{
					spec.GateWidth = value ? 96f : 0f;
					changed?.Invoke();
					refresh?.Invoke();
				} );
			}

			if ( spec.GateWidth <= 0f )
			{
				return;
			}

			group.Add( ArchPartUi.Number( "Gate at", spec.GateAt, 0f, value => spec.GateAt = value, changed ) );
			group.Add( ArchPartUi.Number( "Gate width", spec.GateWidth, 96f, value => spec.GateWidth = value, changed ) );
		} );
	}

	static void Slabs( ToolSidebarWidget panel, ArchBarrierSpec spec, Action changed )
	{
		ArchSidebarSection.Disclosure( panel, "Barrier.slabs", "Slabs", true, group =>
		{
			group.Add( ArchPartUi.Integer( "Courses", spec.Courses, 4, value => spec.Courses = value, changed ) );
			group.Add( ArchPartUi.Number( "Slab thickness", spec.PanelThickness, 2.4f, value => spec.PanelThickness = value, changed ) );
			group.Add( ArchPartUi.Number( "Plinth height", spec.PlinthHeight, 0f, value => spec.PlinthHeight = value, changed ) );
			group.Add( ArchPartUi.Number( "Post overhang", spec.PostOverhang, 0f, value => spec.PostOverhang = value, changed ) );
		} );
	}

	// No brackets, no strands to hang off them - the bracket count is what stands the topping up at all.
	static void Electric( ToolSidebarWidget panel, ArchBarrierSpec spec, Action refresh, Action changed )
	{
		ArchSidebarSection.Show( panel, "Barrier.electric", "Electric topping", group =>
		{
			using ( var grid = ArchIconGrid.In( group ) )
			{
				grid.Toggle( "Live strands on brackets over the wall", "opt_electric", "bolt", spec.Droppers > 0, value =>
				{
					spec.Droppers = value ? 2 : 0;
					changed?.Invoke();
					refresh?.Invoke();
				} );
			}

			if ( spec.Droppers <= 0 )
			{
				return;
			}

			group.Add( ArchPartUi.Integer( "Brackets", spec.Droppers, 2, value => spec.Droppers = value, changed ) );
			group.Add( ArchPartUi.Integer( "Strands", spec.Strands, 6, value => spec.Strands = value, changed ) );
			group.Add( ArchPartUi.Number( "Bracket lean", spec.DropperLean, 35f, value => spec.DropperLean = value, changed ) );
			group.Add( ArchPartUi.Number( "Bracket length", spec.DropperLength, 16f, value => spec.DropperLength = value, changed ) );
		} );
	}

	static void Infill( ToolSidebarWidget panel, ArchBarrierSpec spec, Action changed )
	{
		ArchSidebarSection.Show( panel, "Barrier.infill", "Infill", group =>
		{
			// A catenary's infill IS its conductors, and how far they sag comes off the kit as a share of the span.
			if ( spec.Style == BarrierStyle.Catenary )
			{
				group.Add( ArchPartUi.Integer( "Strands", spec.Strands, 6, value => spec.Strands = value, changed ) );

				return;
			}

			group.Add( ArchPartUi.Integer( "Rails", spec.Rails, 2, value => spec.Rails = value, changed ) );

			if ( spec.Style != BarrierStyle.Picket )
			{
				return;
			}

			using var grid = ArchIconGrid.In( group );

			foreach ( var value in Enum.GetValues<PicketTop>() )
			{
				var captured = value;

				grid.Pick( $"Top: {captured}", $"picket_{captured}".ToLowerInvariant(), TopIcon( captured ),
					spec.Top == captured, () => { spec.Top = captured; changed?.Invoke(); } );
			}
		} );
	}

	// Wear is judged after a run is standing, never before it is drawn, so it opens folded.
	static void Disrepair( ToolSidebarWidget panel, ArchBarrierSpec spec, Action changed )
	{
		ArchSidebarSection.Disclosure( panel, "Barrier.disrepair", "Disrepair", true, group =>
		{
			group.Add( ArchPartUi.Integer( "Seed", spec.Seed, 0, value => spec.Seed = value, changed ) );
			group.Add( ArchPartUi.Number( "Wear", spec.Wear, 0f, value => spec.Wear = value, changed ) );
			group.Add( ArchPartUi.Number( "Broken", spec.Broken, 0f, value => spec.Broken = value, changed ) );
			group.Add( ArchPartUi.Integer( "Course vary", spec.CourseVary, 0, value => spec.CourseVary = value, changed ) );
		} );
	}

	public static string Icon( BarrierStyle style ) => style switch
	{
		BarrierStyle.PrecastSlab => "view_week",
		BarrierStyle.Palisade => "density_medium",
		BarrierStyle.Guardrail => "horizontal_split",
		BarrierStyle.PostAndRail => "table_rows",
		BarrierStyle.Bollards => "more_vert",
		BarrierStyle.Retaining => "foundation",
		BarrierStyle.Catenary => "power_input",
		_ => "fence"
	};

	static string GroundIcon( BarrierGround ground ) => ground switch
	{
		BarrierGround.Step => "stairs",
		BarrierGround.Level => "horizontal_rule",
		_ => "landscape"
	};

	static string TopIcon( PicketTop top ) => top switch
	{
		PicketTop.Pointed => "change_history",
		PicketTop.Flat => "horizontal_rule",
		_ => "architecture"
	};
}