Editor/Porch/ArchPorchUi.cs
using System;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// One sheet, read by the Porch subtool's draft AND by Select, off the same record - which is the whole
// reason a placed porch can now be given a roof at all. The roof used to be a flag on the subtool, so it
// could only ever be answered while the mouse was still down.
public static class ArchPorchUi {
// A tick that GATES the fields under it has to rebuild the sheet as well as commit, or the fields it
// reveals never arrive and the ones it hides stay put.
static Action Both( Action changed, Action refresh ) {
return () => {
changed?.Invoke();
refresh?.Invoke();
};
}
// Each form takes a Layout rather than a sidebar, so the tool can ask them one per step and the sheet can lay
// them all out in a row.
public static void Standing( Layout group, ArchPorchPart porch, Action refresh, Action changed ) {
var both = Both( changed, refresh );
using var grid = ArchIconGrid.In( group );
grid.Pick( "Against the house — the shell is subtracted, so a drag over a corner wraps it", "porch_against", "home",
porch.Standing == PorchStanding.Against, () => { porch.Standing = PorchStanding.Against; both(); } );
grid.Pick( "Free-standing — the drag IS the deck, which is how one stands inside a room", "porch_free", "crop_free",
porch.Standing == PorchStanding.Free, () => { porch.Standing = PorchStanding.Free; both(); } );
}
// Roofed or not is one two-state answer, so it is a pick rather than a lone tick - the pitch only means
// anything under the roofed half of it.
public static void Roof( Layout group, ArchPorchPart porch, Action refresh, Action changed ) {
var both = Both( changed, refresh );
using ( var grid = ArchIconGrid.In( group ) ) {
grid.Pick( "Roofed — a section re-cut from where the legs stand, tucked under whatever eave hangs over it",
"opt_porch_roof", "roofing", porch.Roofed, () => { porch.Roofed = true; both(); } );
grid.Pick( "Open — a deck with nothing over it", "opt_porch_open", "deck",
!porch.Roofed, () => { porch.Roofed = false; both(); } );
}
if ( !porch.Roofed ) {
return;
}
group.Add( ArchPartUi.Number( "Pitch", porch.Pitch, 0f, value => porch.Pitch = value, changed ) );
}
public static void Include( Layout group, ArchPorchPart porch, Action changed ) {
using var grid = ArchIconGrid.In( group );
grid.Toggle( "Deck under it — off leaves a canopy over open ground", "opt_porch_deck", "layers", porch.Deck,
value => { porch.Deck = value; changed?.Invoke(); } );
grid.Toggle( "Exposed rafters instead of a lined ceiling", "opt_rafters", "reorder", porch.Rafters,
value => { porch.Rafters = value; changed?.Invoke(); } );
grid.Toggle( "Raking struts back to the wall", "opt_braces", "call_made", porch.Braces,
value => { porch.Braces = value; changed?.Invoke(); } );
grid.Toggle( "Railing on the open edges", "opt_railing", "fence", porch.Railings,
value => { porch.Railings = value; changed?.Invoke(); } );
grid.Toggle( "Door through the wall it fronts — the way in off the deck", "opt_porch_door", "door_front", porch.Door,
value => { porch.Door = value; changed?.Invoke(); } );
}
public static void Frame( Layout group, ArchPorchPart porch, Action changed ) {
using ( var grid = ArchIconGrid.In( group ) ) {
grid.Toggle( "Plinth under the deck", "opt_plinth", "vertical_align_bottom", porch.Plinth,
value => { porch.Plinth = value; changed?.Invoke(); } );
grid.Toggle( "Posts on the open perimeter", "opt_posts", "view_column", porch.Posts,
value => { porch.Posts = value; changed?.Invoke(); } );
grid.Toggle( "Header beam over the posts", "opt_beam", "horizontal_rule", porch.Beam,
value => { porch.Beam = value; changed?.Invoke(); } );
}
group.Add( ArchPartUi.Number( "Post spacing", porch.PostSpacing, 96f, value => porch.PostSpacing = value, changed ) );
}
public static void Placement( Layout group, ArchPorchPart porch, Action changed ) {
group.Add( ArchPartUi.Number( "Deck drop", porch.DeckDrop, 24f,
value => porch.GradeHeight = porch.BaseHeight - value, changed ) );
group.Add( ArchPartUi.Number( "Head height", porch.HeadHeight, 100f, value => porch.HeadHeight = value, changed ) );
}
public static string StandingName( ArchPorchPart porch ) => porch.Standing == PorchStanding.Against ? "Against" : "Free-standing";
public static string StandingGlyph( ArchPorchPart porch ) => porch.Standing == PorchStanding.Against ? "home" : "crop_free";
public static void Build( ToolSidebarWidget panel, ArchPorchPart porch, Action refresh, Action changed ) {
Standing( panel.AddGroup( "Standing" ), porch, refresh, changed );
ArchSidebarSection.Show( panel, "Porch.placement", "Placement", group => Placement( group, porch, changed ) );
ArchSidebarSection.Show( panel, "Porch.roof", "Roof", group => Roof( group, porch, refresh, changed ) );
ArchSidebarSection.Show( panel, "Porch.include", "Include", group => Include( group, porch, changed ) );
}
public static void Sizes( ToolSidebarWidget panel, ArchPorchPart porch, Action changed ) {
ArchSidebarSection.Disclosure( panel, "Porch.frame", "Frame", true, group => Frame( group, porch, changed ) );
}
// The placement draft has no plan to file a flight into, so the button only exists on a standing porch.
// A flight is a REAL layer here - it gets a row, handles and a sheet - and the railing's gap is read back
// off it, so adding a second way down needs nothing else said anywhere.
public static void Steps( ToolSidebarWidget panel, ArchTool tool, ArchSelection picked, ArchPorchPart porch, Action refresh, Action changed ) {
ArchSidebarSection.Show( panel, "Porch.steps", "Steps", group => {
group.Add( ArchPartUi.Wrapped( porch.Stairs.Count == 0
? "No way down. A flight added here becomes its own row — drag it to move it along the deck."
: $"{porch.Stairs.Count} flight{(porch.Stairs.Count == 1 ? "" : "s")} off this deck, each its own row." ) );
if ( porch.DeckDrop < 4f ) {
group.Add( ArchPartUi.Wrapped( "The deck stands too close to grade for a flight." ) );
return;
}
group.Add( new Button( "Add a flight", "stairs" ) {
Clicked = () => {
var shape = ArchPorchShape.Resolve( porch, picked.Room, picked.Building, tool.Kit );
if ( ArchPorchSteps.Stand( tool.Plan, porch, shape, tool.Kit ) is null ) {
Log.Info( "Architecture: that porch has no open edge left to bring a flight down through." );
return;
}
changed?.Invoke();
refresh?.Invoke();
}
} );
} );
}
}
public sealed class ArchPorchSheet : IArchSheet {
public Type Payload => typeof( ArchPorchPart );
public void Build( ToolSidebarWidget panel, ArchTool tool, ArchSelection picked, Action refresh, Action changed ) {
if ( picked.Item is not ArchPorchPart porch ) {
return;
}
ArchPorchUi.Build( panel, porch, refresh, changed );
ArchPorchUi.Sizes( panel, porch, changed );
ArchPorchUi.Steps( panel, tool, picked, porch, refresh, changed );
}
}