Editor/Tool/Subtools/ArchBarrierUi.cs
using System;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// Read by the Fence subtool, by a road's own barrier and by a bridge parapet, off the same record. Each form
// takes a Layout rather than a sidebar, so a tool can ask them one per step and a host that still stacks
// sections can lay them all out in a row.
public static class ArchBarrierUi {
// A barrier's own rows, added to the HOST's sequence rather than opened as a second one - the style decides
// which of them exist at all, and that branch belongs in one place rather than in every tool that shows a run.
public static void Steps( ArchWorkflow flow, ArchBarrierSpec spec, Action refresh, Action changed ) {
flow.Step( "Barrier", Label( spec.Style ), Icon( spec.Style ), step => Styles( step.Layout, spec, step.Chose, changed ) );
flow.Step( "Ground", spec.Ground.ToString(), GroundIcon( spec.Ground ), step => Grounds( step.Layout, spec, step.Chose, changed ) );
if ( spec.Style != BarrierStyle.Retaining ) {
flow.Step( "Gate", spec.GateWidth > 0f ? $"{spec.GateWidth:0} wide" : "None",
spec.GateWidth > 0f ? "meeting_room" : "block", step => Gate( step.Layout, spec, refresh, changed ) );
Dressing( flow, spec, refresh, changed );
}
flow.Step( "Bays", BayName( spec ), "view_column", step => Bays( step.Layout, spec, changed ) );
flow.Step( "Disrepair", WearName( spec ), "broken_image", step => Disrepair( step.Layout, spec, changed ) );
}
static void Dressing( ArchWorkflow flow, ArchBarrierSpec spec, Action refresh, Action changed ) {
if ( spec.Style == BarrierStyle.PrecastSlab ) {
flow.Step( "Slabs", $"{spec.Courses} courses", "view_week", step => Slabs( step.Layout, spec, changed ) );
flow.Step( "Topping", spec.Droppers > 0 ? $"{spec.Strands} strands" : "None",
spec.Droppers > 0 ? "bolt" : "block", step => Electric( step.Layout, spec, refresh, changed ) );
return;
}
if ( spec.Style == BarrierStyle.Solid ) {
flow.Step( "Parapet", spec.Coping ? "Coped" : "Bare", spec.Coping ? "layers" : "rectangle",
step => Parapet( step.Layout, spec, refresh, changed ) );
return;
}
flow.Step( "Infill", InfillName( spec ), InfillGlyph( spec ), step => Infill( step.Layout, spec, step.Chose, changed ) );
}
public static void Build( ToolSidebarWidget panel, ArchBarrierSpec spec, Action refresh, Action changed = null ) {
ArchSidebarSection.Show( panel, "Barrier.style", "Barrier", group => {
Styles( group, spec, refresh, changed );
Grounds( group, spec, refresh, changed );
} );
ArchSidebarSection.Show( panel, "Barrier.bays", "Bays", group => Bays( group, spec, changed ) );
if ( spec.Style == BarrierStyle.Retaining ) {
ArchSidebarSection.Disclosure( panel, "Barrier.disrepair", "Disrepair", true, group => Disrepair( group, spec, changed ) );
return;
}
ArchSidebarSection.Show( panel, "Barrier.gate", "Gate", group => Gate( group, spec, refresh, changed ) );
if ( spec.Style == BarrierStyle.PrecastSlab ) {
ArchSidebarSection.Disclosure( panel, "Barrier.slabs", "Slabs", true, group => Slabs( group, spec, changed ) );
ArchSidebarSection.Show( panel, "Barrier.electric", "Electric topping", group => Electric( group, spec, refresh, changed ) );
} else if ( spec.Style == BarrierStyle.Solid ) {
ArchSidebarSection.Show( panel, "Barrier.parapet", "Parapet", group => Parapet( group, spec, refresh, changed ) );
} else {
ArchSidebarSection.Show( panel, "Barrier.infill", "Infill", group => Infill( group, spec, refresh, changed ) );
}
ArchSidebarSection.Disclosure( panel, "Barrier.disrepair", "Disrepair", true, group => Disrepair( group, spec, changed ) );
}
// The three a deck, a balcony or a roof edge may stand, offered wherever one of them has a guard turned on -
// so a parapet is picked the same way on all of them and no host grows a style list of its own.
public static void GuardStyles( Layout group, BarrierStyle style, Action<BarrierStyle> set ) {
using var grid = ArchIconGrid.In( group );
foreach ( var standing in ArchGuardStyles.Standing ) {
var captured = standing;
grid.Pick( ArchGuardStyles.Hint( captured ), $"guard_{captured}".ToLowerInvariant(), ArchGuardStyles.Icon( captured ),
style == captured, () => set( captured ) );
}
}
public static void Styles( Layout group, ArchBarrierSpec spec, Action refresh, Action changed ) {
using var grid = ArchIconGrid.In( group );
foreach ( var style in Enum.GetValues<BarrierStyle>() ) {
var captured = style;
grid.Pick( $"{Label( captured )} — {Hint( captured )}", $"barrier_{captured}".ToLowerInvariant(), Icon( captured ),
spec.Style == captured, () => { spec.Style = captured; changed?.Invoke(); refresh?.Invoke(); } );
}
}
public static void Grounds( Layout group, ArchBarrierSpec spec, Action refresh, Action changed ) {
using var grid = ArchIconGrid.In( group );
foreach ( var ground in Enum.GetValues<BarrierGround>() ) {
var captured = ground;
grid.Pick( $"{captured} — {GroundHint( captured )}", $"ground_{captured}".ToLowerInvariant(), GroundIcon( captured ),
spec.Ground == captured, () => { spec.Ground = captured; changed?.Invoke(); refresh?.Invoke(); } );
}
}
public static void Bays( Layout group, ArchBarrierSpec spec, Action changed ) {
var retaining = spec.Style == BarrierStyle.Retaining;
// 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 ) );
if ( spec.Style != BarrierStyle.Solid ) {
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.
public static void Gate( Layout group, ArchBarrierSpec spec, Action refresh, Action changed ) {
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 ) );
}
public static void Slabs( Layout group, ArchBarrierSpec spec, Action changed ) {
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.
public static void Electric( Layout group, ArchBarrierSpec spec, Action refresh, Action changed ) {
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 ) );
}
// A plastered boundary wall: the section and the cap, and nothing about posts or rails it does not have.
public static void Parapet( Layout group, ArchBarrierSpec spec, Action refresh, Action changed ) {
group.Add( ArchPartUi.Number( "Wall thickness", spec.PanelThickness, 6f, value => spec.PanelThickness = value, changed ) );
using ( var grid = ArchIconGrid.In( group ) ) {
grid.Toggle( "Coping over the head, oversailing both faces", "opt_parapet_coping", "layers", spec.Coping,
value => { spec.Coping = value; changed?.Invoke(); refresh?.Invoke(); } );
}
if ( !spec.Coping ) {
return;
}
group.Add( ArchPartUi.Number( "Coping height", spec.CopingHeight, 2.5f, value => spec.CopingHeight = value, changed ) );
group.Add( ArchPartUi.Number( "Coping oversail", spec.CopingProud, 1.2f, value => spec.CopingProud = value, changed ) );
}
public static void Infill( Layout group, ArchBarrierSpec spec, Action refresh, Action changed ) {
// 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(); refresh?.Invoke(); } );
}
}
// Wear is judged after a run is standing, never before it is drawn, so it is asked last.
public static void Disrepair( Layout group, ArchBarrierSpec spec, Action changed ) {
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 ) );
}
// Ten characters or fewer, every one of them: a tile caption is derived from the name before the dash, and
// ArchIconGrid keeps only the first word of anything longer.
public static string Label( BarrierStyle style ) => style switch {
BarrierStyle.PrecastSlab => "Precast",
BarrierStyle.PostAndRail => "Post rail",
BarrierStyle.Solid => "Parapet",
_ => style.ToString()
};
static string Hint( BarrierStyle style ) => style switch {
BarrierStyle.PrecastSlab => "stacked slabs between posts, the township boundary wall",
BarrierStyle.Palisade => "vertical steel pales on rails",
BarrierStyle.Guardrail => "a crash barrier on posts",
BarrierStyle.PostAndRail => "open horizontal rails",
BarrierStyle.Bollards => "free-standing posts, no infill",
BarrierStyle.Retaining => "a battered wall holding the grade behind it",
BarrierStyle.Catenary => "conductors sagging between poles",
BarrierStyle.Solid => "a plastered wall with an optional coping",
_ => "pickets on rails between posts"
};
static string GroundHint( BarrierGround ground ) => ground switch {
BarrierGround.Step => "every bay level, dropping between them",
BarrierGround.Level => "the fall ignored altogether",
_ => "each bay tilted to follow the slope"
};
static string BayName( ArchBarrierSpec spec ) => spec.Style == BarrierStyle.Retaining
? $"{spec.PanelLength:0} pitch"
: $"{spec.Height:0} high";
static string WearName( ArchBarrierSpec spec ) => spec.Wear > 0f || spec.Broken > 0f
? $"{spec.Wear:0.##} wear"
: "None";
static string InfillName( ArchBarrierSpec spec ) => spec.Style switch {
BarrierStyle.Catenary => $"{spec.Strands} strands",
BarrierStyle.Picket => $"{spec.Rails} rails, {spec.Top}",
_ => $"{spec.Rails} rails"
};
static string InfillGlyph( ArchBarrierSpec spec ) => spec.Style switch {
BarrierStyle.Catenary => "power_input",
BarrierStyle.Picket => TopIcon( spec.Top ),
_ => "table_rows"
};
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",
BarrierStyle.Solid => "rectangle",
_ => "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"
};
}