Editor UI builder for wall treatments. It constructs sidebar widgets for editing wall properties (sizes, faces, coping, cladding, bands, trims, openings rhythm, pilasters) and wires UI controls to IArchWallTreatment data and ArchKit presets.
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// Read by Wall, Select and both designer forms off the same IArchWallTreatment.
public static class ArchWallUi
{
// Not every ArchSurface - most aren't wall at all; nobody could choose from all.
static readonly ArchSurface[] Fields =
{
ArchSurface.WallBase,
ArchSurface.Foundation,
ArchSurface.WallExterior,
ArchSurface.Panel,
ArchSurface.Siding,
ArchSurface.Pillar
};
// What a thin proud strip is worth being made of.
static readonly ArchSurface[] Strips =
{
ArchSurface.Trim,
ArchSurface.Gutter,
ArchSurface.Railing,
ArchSurface.WallCap,
ArchSurface.WallBase,
ArchSurface.Kerb
};
// A creation sidebar puts height and thickness in Placement, where the next drag reads them; every
// other caller is editing a treatment whole and wants them here. A rhythm is spaced over a real
// elevation, so only a caller holding one offers it.
public static void Build( ToolSidebarWidget panel, ArchKit kit, IArchWallTreatment wall, Action refresh, Action changed, bool sizes = true, bool openings = true )
{
if ( sizes )
{
Sizes( panel, kit, wall, changed );
}
Faces( panel, wall, refresh, changed );
Coping( panel, kit, wall, changed );
Cladding( panel, wall, changed );
Bands( panel, kit, wall, refresh, changed );
Trims( panel, kit, wall, refresh, changed );
if ( openings )
{
Rhythm( panel, kit, wall, refresh, changed );
}
Pilasters( panel, wall, refresh, changed );
}
static void Sizes( ToolSidebarWidget panel, ArchKit kit, IArchWallTreatment wall, Action changed )
{
var group = panel.AddGroup( "Wall" );
group.Add( ArchPartUi.Number( "Height", wall.Height, kit.WallHeight, value => wall.Height = value, changed ) );
group.Add( ArchPartUi.Number( "Thickness", wall.Thickness, kit.WallThickness, value => wall.Thickness = value, changed ) );
}
static void Faces( ToolSidebarWidget panel, IArchWallTreatment wall, Action refresh, Action changed )
{
using var grid = ArchIconGrid.In( panel.AddGroup( "Faces" ) );
grid.Toggle( "Exterior — skinned outside as well as in", "opt_exterior", "villa", wall.Exterior,
value => { wall.Exterior = value; changed?.Invoke(); } );
grid.Toggle( "Single-sided — the far face is never emitted, for a silhouette nobody enters",
"opt_single_sided", "filter_1", wall.SingleSided,
value => { wall.SingleSided = value; changed?.Invoke(); } );
grid.Toggle( "Skirting round the room", "opt_baseboard", "border_bottom", wall.Baseboard,
value => { wall.Baseboard = value; changed?.Invoke(); } );
grid.Toggle( "Coping along the top, for a wall standing clear of the ceiling", "opt_wall_cap", "border_top", wall.Cap,
value => { wall.Cap = value; changed?.Invoke(); refresh?.Invoke(); } );
grid.Toggle( "Wainscot up the inside face", "opt_wainscot", "table_rows", wall.Wainscot,
value => { wall.Wainscot = value; changed?.Invoke(); } );
}
// The coping is a proud section like a band course, so it carries the same two dimensions.
static void Coping( ToolSidebarWidget panel, ArchKit kit, IArchWallTreatment wall, Action changed )
{
if ( !wall.Cap )
{
return;
}
var group = panel.AddGroup( "Coping" );
group.Add( ArchPartUi.Number( "Cap height", wall.CapHeight, kit.WallCapHeight, value => wall.CapHeight = value, changed ) );
group.Add( ArchPartUi.Number( "Oversails by", wall.CapOverhang, kit.CasingDepth, value => wall.CapOverhang = value, changed ) );
}
static void Cladding( ToolSidebarWidget panel, IArchWallTreatment wall, Action changed )
{
using var grid = ArchIconGrid.In( panel.AddGroup( "Cladding" ) );
foreach ( var value in Enum.GetValues<WallCladding>() )
{
var captured = value;
grid.Pick( Describe( captured ), $"cladding_{captured}".ToLowerInvariant(), Glyph( captured ),
wall.Cladding == captured, () => { wall.Cladding = captured; changed?.Invoke(); } );
}
}
// A band per row - a real elevation changes field more than once.
static void Bands( ToolSidebarWidget panel, ArchKit kit, IArchWallTreatment wall, Action refresh, Action changed )
{
var group = panel.AddGroup( wall.Bands.Count == 0 ? "Bands" : $"Bands ({wall.Bands.Count})" );
var storey = Storey( kit, wall );
foreach ( var band in ArchWallTreatments.Ordered( wall.Bands, storey ).ToList() )
{
var captured = band;
group.Add( ArchPartUi.Check( "Rides at the wall head — a cap", captured.AtHead,
value => { captured.AtHead = value; changed?.Invoke(); refresh?.Invoke(); }, changed ) );
if ( !captured.AtHead )
{
group.Add( ArchPartUi.Number( "Changes at", captured.Height, 0f, value => captured.Height = value, changed ) );
}
using ( var fields = ArchIconGrid.In( group ) )
{
foreach ( var role in Fields )
{
var wanted = role;
fields.Pick( $"The field below it is {wanted}", $"surface_{wanted}".ToLowerInvariant(), FieldGlyph( wanted ),
captured.Field == wanted, () => { captured.Field = wanted; changed?.Invoke(); } );
}
}
group.Add( ArchPartUi.Check( "Trim standing proud on the change", captured.Course,
value => { captured.Course = value; changed?.Invoke(); }, changed ) );
// Height AND depth - a cap is thicker, finishing the field rather than dividing two.
group.Add( ArchPartUi.Number( "Trim height", captured.CourseHeight, kit.WallBandHeight,
value => captured.CourseHeight = value, changed ) );
group.Add( ArchPartUi.Number( "Stands proud", captured.CourseDepth, kit.WallBandDepth,
value => captured.CourseDepth = value, changed ) );
using ( var seats = ArchIconGrid.In( group ) )
{
seats.Pick( "Straddles the change — a string course between two fields", "band_centred", "horizontal_split",
captured.Seat == BandSeat.Centred, () => { captured.Seat = BandSeat.Centred; changed?.Invoke(); } );
seats.Pick( "Sits on the field below — a capping", "band_above", "vertical_align_top",
captured.Seat == BandSeat.Above, () => { captured.Seat = BandSeat.Above; changed?.Invoke(); } );
}
using var remove = ArchIconGrid.In( group );
remove.Toggle( $"Remove the band at {captured.At( storey ):0}", "band_remove", "delete", false, _ =>
{
wall.Bands.Remove( captured );
changed?.Invoke();
refresh?.Invoke();
} );
}
using var add = ArchIconGrid.In( group );
add.Toggle( "Add a band — the field below it takes its own role", "band_add", "add", false, _ =>
{
wall.Bands.Add( new ArchWallBand { Height = Next( kit, wall ) } );
changed?.Invoke();
refresh?.Invoke();
} );
}
static void Trims( ToolSidebarWidget panel, ArchKit kit, IArchWallTreatment wall, Action refresh, Action changed )
{
var group = panel.AddGroup( wall.Trims.Count == 0 ? "Trim bands" : $"Trim bands ({wall.Trims.Count})" );
var storey = Storey( kit, wall );
foreach ( var trim in ArchWallTreatments.Standing( wall.Trims, storey ).ToList() )
{
var captured = trim;
group.Add( ArchPartUi.Check( "Rides at the wall head", captured.AtHead,
value => { captured.AtHead = value; changed?.Invoke(); refresh?.Invoke(); }, changed ) );
if ( !captured.AtHead )
{
group.Add( ArchPartUi.Number( "Sits at", captured.Height, 96f, value => captured.Height = value, changed ) );
}
group.Add( ArchPartUi.Number( "Band height", captured.Rise, 3f, value => captured.Rise = value, changed ) );
group.Add( ArchPartUi.Number( "Stands proud", captured.Depth, 1.5f, value => captured.Depth = value, changed ) );
group.Add( ArchPartUi.Number( "Bevel", captured.Bevel, 0.75f, value => captured.Bevel = value, changed ) );
using ( var fields = ArchIconGrid.In( group ) )
{
foreach ( var role in Strips )
{
var wanted = role;
fields.Pick( $"Skinned as {wanted}", $"surface_{wanted}".ToLowerInvariant(), FieldGlyph( wanted ),
captured.Field == wanted, () => { captured.Field = wanted; changed?.Invoke(); } );
}
}
using var row = ArchIconGrid.In( group );
row.Toggle( "Runs on the inside face instead", "trim_interior", "flip_to_back", captured.Interior,
value => { captured.Interior = value; changed?.Invoke(); } );
row.Toggle( $"Remove the band at {captured.At( storey ):0}", "trim_remove", "delete", false, _ =>
{
wall.Trims.Remove( captured );
changed?.Invoke();
refresh?.Invoke();
} );
}
using var add = ArchIconGrid.In( group );
add.Toggle( "Add a proud band — anywhere up the wall, in any role", "trim_add", "add", false, _ =>
{
wall.Trims.Add( new ArchWallTrim { Height = NextTrim( kit, wall ) } );
changed?.Invoke();
refresh?.Invoke();
} );
}
// The rhythm the elevation wears. The units it stands are real openings, so the drag stays the way to place
// one that breaks the pattern - the rhythm parts around whatever is already there.
static void Rhythm( ToolSidebarWidget panel, ArchKit kit, IArchWallTreatment wall, Action refresh, Action changed )
{
var run = wall.OpeningRun;
using ( var grid = ArchIconGrid.In( panel.AddGroup( "Openings" ) ) )
{
grid.Toggle( "Wear a rhythm of openings the whole elevation shares, at a bay", "opt_opening_run", "window", run.Stands,
value =>
{
run.Preset = value ? Offered( kit ) : "";
run.Bay = value ? 168f : 0f;
changed?.Invoke();
refresh?.Invoke();
} );
}
if ( !run.Stands )
{
return;
}
var group = panel.AddGroup( "Bay rhythm" );
var unit = kit.Openings.FirstOrDefault( preset => string.Equals( preset.Name, run.Preset, StringComparison.OrdinalIgnoreCase ) );
group.Add( Units( panel, kit, run, changed ) );
group.Add( ArchPartUi.Number( "Bay at most", run.Bay, 168f, value => run.Bay = value, changed ) );
group.Add( ArchPartUi.Integer( "Or exactly this many", run.Count, 0, value => run.Count = value, changed ) );
group.Add( ArchPartUi.Number( "Width", run.Width, unit?.Width ?? 0f, value => run.Width = value, changed ) );
group.Add( ArchPartUi.Number( "Height", run.Height, unit?.Height ?? 0f, value => run.Height = value, changed ) );
group.Add( ArchPartUi.Number( "Sill height", run.SillHeight, unit?.SillHeight ?? 0f, value => run.SillHeight = value, changed ) );
}
static Widget Units( Widget parent, ArchKit kit, ArchOpeningRun run, Action changed )
{
var browser = new ArchPresetBrowser<ArchOpeningPreset>( parent, kit, "wall.rhythm.presets", "Unit" )
{
Chosen = preset => string.Equals( run.Preset, preset.Name, StringComparison.OrdinalIgnoreCase ),
Choose = preset =>
{
run.Preset = preset.Name;
changed?.Invoke();
}
};
browser.Set( kit.Openings
.Select( preset => new ArchPresetItem<ArchOpeningPreset>
{
Value = preset,
Name = preset.Name,
Detail = ArchOpeningStage.Describe( preset ),
Identity = ArchOpeningStage.Identity( preset ),
Category = preset.Kind.ToString(),
Glyph = ArchIcons.OpeningGlyph( preset ),
Tags = preset.Kind.ToString(),
Recommended = preset.Kind == OpeningKind.Window,
View = ArchPresetPreview.Elevation,
Interest = ArchOpeningStage.Interest( preset, kit ),
Recipe = () => ArchOpeningStage.Of( preset, kit )
} )
.ToList() );
return browser;
}
// A rhythm is windows far more often than it is anything else, so switching it on offers one.
static string Offered( ArchKit kit )
{
var wanted = kit.Openings.FirstOrDefault( preset => preset.Kind == OpeningKind.Window )
?? kit.Openings.FirstOrDefault();
return wanted?.Name ?? "";
}
static void Pilasters( ToolSidebarWidget panel, IArchWallTreatment wall, Action refresh, Action changed )
{
var run = wall.Pilasters;
using ( var grid = ArchIconGrid.In( panel.AddGroup( "Pilasters" ) ) )
{
grid.Toggle( "Express the column line outside the cladding, at a bay", "opt_pilasters", "view_week", run.Stands,
value => { run.Bay = value ? 168f : 0f; changed?.Invoke(); refresh?.Invoke(); } );
}
if ( !run.Stands )
{
return;
}
var group = panel.AddGroup( "Column line" );
group.Add( ArchPartUi.Number( "Bay at most", run.Bay, 168f, value => run.Bay = value, changed ) );
group.Add( ArchPartUi.Integer( "Or exactly this many", run.Count, 0, value => run.Count = value, changed ) );
group.Add( ArchPartUi.Number( "Strip width", run.Width, 12f, value => run.Width = value, changed ) );
group.Add( ArchPartUi.Number( "Stands proud", run.Depth, 3f, value => run.Depth = value, changed ) );
group.Add( ArchPartUi.Number( "Starts above the foot", run.Foot, 0f, value => run.Foot = value, changed ) );
group.Add( ArchPartUi.Number( "Stops short of the head", run.Headroom, 0f, value => run.Headroom = value, changed ) );
using var corners = ArchIconGrid.In( group );
corners.Toggle( "End strips wrap the corner instead of being held inboard of it", "pilaster_corners", "border_outer",
run.OnCorners, value => { run.OnCorners = value; changed?.Invoke(); } );
}
static float Storey( ArchKit kit, IArchWallTreatment wall ) => wall.Height > 1f ? wall.Height : kit.WallHeight;
// Clear of the one below, so three read as stripes, on a grid line not a fraction.
static float NextTrim( ArchKit kit, IArchWallTreatment wall )
{
var height = Storey( kit, wall );
var standing = ArchWallTreatments.Standing( wall.Trims, height ).LastOrDefault();
var grid = new ArchGridService();
var wanted = standing is null
? height * 0.4f
: MathF.Min( standing.At( height ) + standing.Rise + height * 0.15f, height * 0.92f );
return MathF.Max( grid.SubgridSize(), grid.Subgrid( wanted ) );
}
// Above the highest band, or at dado height on a bare wall.
static float Next( ArchKit kit, IArchWallTreatment wall )
{
var height = Storey( kit, wall );
var standing = ArchWallTreatments.Ordered( wall.Bands, height ).LastOrDefault();
return standing is null
? ArchGridService.Fine( height * 0.34f )
: ArchGridService.Fine( MathF.Min( standing.At( height ) + height * 0.25f, height * 0.9f ) );
}
public static string Describe( WallCladding cladding ) => cladding switch
{
WallCladding.Weatherboard => "Weatherboard — lapped horizontal courses",
WallCladding.Profiled => "Profiled sheeting — vertical trapezoidal ribs",
_ => "None — the field is flat"
};
static string Glyph( WallCladding cladding ) => cladding switch
{
WallCladding.Weatherboard => "view_stream",
WallCladding.Profiled => "view_week",
_ => "crop_square"
};
static string FieldGlyph( ArchSurface field ) => field switch
{
ArchSurface.Foundation => "foundation",
ArchSurface.WallBase => "grid_4x4",
ArchSurface.Panel => "crop_square",
ArchSurface.Siding => "view_stream",
ArchSurface.Pillar => "view_column",
ArchSurface.Trim => "horizontal_rule",
ArchSurface.Gutter => "water_damage",
ArchSurface.Railing => "fence",
ArchSurface.WallCap => "border_top",
ArchSurface.Kerb => "square_foot",
_ => "villa"
};
}