Editor/Roof/ArchRoofUi.cs
using System;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// ArchBuild.Roof derives shape and trims from the kit - placement controls would be overwritten.
public static class ArchRoofUi {
// The tool is passed only where a roof is really STANDING - a draft has nothing to convert, so the sidebar that
// seeds the next placement is offered no action that edits one.
public static void Build( ToolSidebarWidget panel, ArchRoofPart roof, ArchKit kit, Action refresh, Action changed, ArchTool tool = null ) {
ArchSidebarSection.Show( panel, "Roof.style", "Roof style", group => {
using var grid = ArchIconGrid.In( group );
foreach ( var value in Enum.GetValues<RoofStyle>() ) {
var captured = value;
grid.Pick( captured.ToString(), ArchIcons.RoofStyleSlug( captured ), ArchIcons.RoofStyleGlyph( captured ),
roof.Style == captured, () => { roof.Style = captured; refresh?.Invoke(); changed?.Invoke(); } );
}
} );
ArchSidebarSection.Show( panel, "Roof.edges", "Edges", group => {
using var edges = ArchIconGrid.In( group );
edges.Toggle( "Parapet wall cap", "opt_parapet", "crop_din", roof.Parapet,
value => { roof.Parapet = value; refresh?.Invoke(); changed?.Invoke(); } );
// A parapet stands where the gutter would hang, so the deck drains inside it and ArchBuild.Roof
// refuses gutters outright - offering the toggle would be offering nothing.
if ( !roof.Parapet ) {
edges.Toggle( "Gutters", "opt_gutters", "water_damage", roof.Gutters,
value => { roof.Gutters = value; changed?.Invoke(); } );
}
edges.Toggle( "Guardrail round the roof edge", "opt_guardrail", "fence", roof.Guardrail,
value => { roof.Guardrail = value; refresh?.Invoke(); changed?.Invoke(); } );
} );
Cornice( panel, roof, kit, changed );
Parapet( panel, roof, refresh, tool );
}
// ONE choice, and the stack behind it is the kit's - the same shelf a wall head crowns itself from, so a
// parapet stood as walls afterwards keeps wearing what the ring wore. It stands as its own section whether or
// not a cornice is on, because the roof/wall corner is what an author comes to this sheet to dress.
static void Cornice( ToolSidebarWidget panel, ArchRoofPart roof, ArchKit kit, Action changed ) {
if ( kit is null ) {
return;
}
ArchSidebarSection.Show( panel, "Roof.cornice", "Cornice", group => {
group.Add( ArchCorniceUi.Picker( panel, kit, "roof.cornices", () => roof.Cornice,
name => { roof.Cornice = name; changed?.Invoke(); } ) );
} );
}
// The ring is ONE height all the way round; standing it as walls is how it comes to vary. Each edge becomes a
// real wall from there - raise it, drop it, dress it in brick, put a gate through it, delete one - and the
// uniform ring switches itself off in the same breath so the deck never carries both.
static void Parapet( ToolSidebarWidget panel, ArchRoofPart roof, Action refresh, ArchTool tool ) {
if ( tool is null ) {
return;
}
ArchSidebarSection.Show( panel, "Roof.parapet", "Parapet", group => {
if ( roof.Walls.Count > 0 ) {
group.Add( ArchPartUi.Wrapped( $"{roof.Walls.Count} wall{(roof.Walls.Count == 1 ? "" : "s")} standing on this deck."
+ " Pick one in Plan Layers to change its height or dress it, or draw another with the Wall tool." ) );
return;
}
group.Add( new Button( "Stand the parapet as walls", "fence" ) {
ToolTip = "One wall per edge of the deck, so each can be raised, dressed or removed on its own",
Clicked = () => {
var stood = ArchRoofWalls.StandParapet( tool.Plan, roof, tool.Kit, ArchWallPreset.Copy( tool.Kit.Walls.FirstOrDefault() ) );
if ( stood.Count == 0 ) {
Log.Info( $"Architecture: {roof.Name} has no outline to run a parapet round." );
return;
}
tool.Commit( $"Stand parapet on {roof.Name}" );
refresh?.Invoke();
}
} );
} );
}
// The same form the Roof subtool drafts a light or a plant item with and the Select subtool edits a placed one
// through. Two numbers for all six forms: what they MEAN and which of them a form even has is
// ArchRoofLightKinds' answer, so a chimney is never offered a pane width it would ignore. The footprint is
// never here - it is dragged on the deck.
public static void Light( ToolSidebarWidget panel, ArchRoofLightPart light, ArchKit kit, Action refresh, Action changed ) {
var form = light.Form;
ArchSidebarSection.Show( panel, "Roof.light", form.Label(), group => {
group.Add( ArchPartUi.Number( form.RiseLabel(), light.Curb, form.RiseStock( kit ), value => light.Curb = value, changed ) );
group.Add( ArchPartUi.Number( form.ReachLabel(), light.CurbWidth, form.ReachStock( kit ), value => light.CurbWidth = value, changed ) );
if ( !form.Glazes() ) {
return;
}
group.Add( ArchPartUi.Check( "Glazed", light.Glazed, value => { light.Glazed = value; refresh?.Invoke(); changed?.Invoke(); } ) );
if ( light.Glazed ) {
group.Add( ArchPartUi.Number( "Widest pane", light.PaneSpan, form.PaneStock(), value => light.PaneSpan = value, changed ) );
}
} );
}
// The one answer to whether a style has a ridge at all, so the creation sidebar and the property sheet
// cannot disagree about whether to offer the row.
// Auto lays the ridge down the longer side - only meaningful at placement.
public static void Ridge( ToolSidebarWidget panel, ArchRoofPart roof, Action changed ) {
if ( !ArchRoofPlane.Ridged( roof.Style ) ) {
return;
}
ArchSidebarSection.Show( panel, "Roof.ridge", "Ridge", group => {
using var grid = ArchIconGrid.In( group );
Direction( grid, RidgeRun.AlongX, roof.RidgeAlongX, () => { roof.RidgeAlongX = true; changed?.Invoke(); } );
Direction( grid, RidgeRun.AlongY, !roof.RidgeAlongX, () => { roof.RidgeAlongX = false; changed?.Invoke(); } );
grid.Toggle( "Shed the other way", "opt_reversed", "swap_vert", roof.Reversed,
value => { roof.Reversed = value; changed?.Invoke(); } );
} );
}
// The shape is what a standing roof is judged on, so it opens; the boards and the lining under it are a
// second pass over one already standing and fold away.
public static void Details( ToolSidebarWidget panel, ArchRoofPart roof, Action changed ) {
ArchSidebarSection.Show( panel, "Roof.shape", "Shape", shape => {
shape.Add( ArchPartUi.Number( "Eave height", roof.BaseHeight, 128f, value => roof.BaseHeight = value, changed ) );
// A flat deck falls nowhere, so the row would be a number that decides nothing. Drag an edge down
// instead and the section becomes a shed carrying the pitch that drop asked for.
if ( roof.Style != RoofStyle.Flat ) {
shape.Add( ArchPartUi.Number( "Pitch", roof.Pitch, 30f, value => roof.Pitch = value, changed ) );
}
shape.Add( ArchPartUi.Number( "Overhang", roof.Overhang, 16f, value => roof.Overhang = value, changed ) );
shape.Add( ArchPartUi.Number( roof.Style == RoofStyle.Flat ? "Slab thickness" : "Thickness", roof.Thickness, 6f, value => roof.Thickness = MathF.Max( 1f, value ), changed ) );
if ( roof.Style == RoofStyle.Sawtooth ) {
shape.Add( ArchPartUi.Integer( "Bays", roof.SawtoothBays, 4, value => roof.SawtoothBays = value, changed ) );
}
if ( roof.Parapet ) {
shape.Add( ArchPartUi.Number( "Parapet height", roof.ParapetHeight, 18f, value => roof.ParapetHeight = value, changed ) );
}
if ( roof.Guardrail ) {
shape.Add( ArchPartUi.Number( "Guardrail height", roof.GuardHeight, 42f, value => roof.GuardHeight = value, changed ) );
ArchBarrierUi.GuardStyles( shape, roof.GuardStyle, style => {
roof.GuardStyle = style;
changed?.Invoke();
} );
}
if ( roof.Frame ) {
shape.Add( ArchPartUi.Number( "Rafter spacing", roof.FrameSpacing, 0f, value => roof.FrameSpacing = value, changed ) );
}
} );
ArchSidebarSection.Disclosure( panel, "Roof.trims", "Trims", true, group => {
using var grid = ArchIconGrid.In( group );
grid.Toggle( "Exposed rafters and purlins under the deck", "opt_roof_frame", "reorder", roof.Frame,
value => { roof.Frame = value; changed?.Invoke(); } );
grid.Toggle( "Fascia along the eaves", "opt_fascia", "border_bottom", roof.Fascia,
value => { roof.Fascia = value; changed?.Invoke(); } );
grid.Toggle( "Boxed soffit under the overhang", "opt_soffit", "border_outer", roof.Soffit,
value => { roof.Soffit = value; changed?.Invoke(); } );
grid.Toggle( "Capping over the ridge and hips", "opt_ridge_cap", "roofing", roof.RidgeCap,
value => { roof.RidgeCap = value; changed?.Invoke(); } );
grid.Toggle( "End walls closing the roof volume", "opt_end_walls", "square", roof.EndWalls,
value => { roof.EndWalls = value; changed?.Invoke(); } );
} );
ArchSidebarSection.Disclosure( panel, "Roof.inside", "Inside", true, group => {
using var inside = ArchIconGrid.In( group );
inside.Toggle( "Ceiling under the wall plate, with a cornice", "opt_roof_ceiling", "border_top", roof.Ceiling,
value => { roof.Ceiling = value; changed?.Invoke(); } );
inside.Toggle( "Skin that ceiling as the floor of a loft", "opt_loft_floor", "layers", roof.LoftFloor,
value => { roof.LoftFloor = value; changed?.Invoke(); } );
} );
}
static void Direction( ArchIconGrid grid, RidgeRun run, bool active, Action chosen ) {
grid.Pick( ArchIcons.RidgeAdvice( run ), ArchIcons.RidgeSlug( run ),
ArchIcons.RidgeGlyph( run ), active, chosen );
}
}