Editor UI for roof parts in the architecture tool. It builds sidebar sections and controls for roof style, edges (parapet, gutters, guardrail), parapet conversion to walls, roof lights, ridge direction, and various shape and trim properties, wiring UI actions to update the ArchRoofPart and trigger refresh/changed callbacks.
using System;
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, 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(); } );
} );
Parapet( panel, roof, refresh, tool );
}
// 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 ) );
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( "Thickness", roof.Thickness, 6f, value => roof.Thickness = 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 ) );
}
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 );
}
}