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.
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"
};
}