Editor/UI/ArchCorniceUi.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
namespace Sunless.Architecture;
// The picker's card, and the course-by-course form behind the Design button. The sheet gets ONE combo; every
// dimension a cornice has lives in here, reached deliberately, because the stack was proportioned together and a
// row of numbers on the elevation is six ways to pull it apart.
public static class ArchCorniceUi {
// What a course is worth being made of. Not every ArchSurface - most are not trim at all.
static readonly ArchSurface[] Fields =
{
ArchSurface.Trim,
ArchSurface.WallCap,
ArchSurface.WallExterior,
ArchSurface.Pillar,
ArchSurface.WallBase,
ArchSurface.Panel
};
public static IEnumerable<ArchCorniceStyle> Standing( ArchKit kit ) {
return kit.Cornices.Where( style => !ArchCorniceStage.Hidden( style ) );
}
public static ArchPresetItem<ArchCorniceStyle> Card( ArchCorniceStyle style, ArchKit kit ) {
return new ArchPresetItem<ArchCorniceStyle> {
Value = style,
Name = style.Name,
Detail = ArchCorniceStage.Describe( style ),
Identity = ArchCorniceStage.Identity( style ),
Category = style.Pier.Stands ? "Piered" : "Plain",
Glyph = "horizontal_split",
Tags = string.Join( " ", style.Courses.Select( course => course.Kind.ToString() ).Distinct() ),
View = ArchPresetPreview.Raised,
Interest = ArchCorniceStage.Interest( style, kit ),
Recipe = () => ArchCorniceStage.Of( style, kit )
};
}
// THE cornice chooser, wherever a head is crowned - a roof edge, a wall head, a parapet. Not having one is a
// card in the same grid rather than a toggle in some other group: the entablature is the loudest thing an
// elevation gets out of this tool, and it spent its life behind one unlabelled icon nobody found.
public static Widget Picker( Widget parent, ArchKit kit, string stateKey, Func<string> current, Action<string> chosen ) {
var browser = new ArchPresetBrowser<ArchCorniceStyle>( parent, kit, stateKey, "Cornice" ) {
Chosen = style => style is null
? kit.FindCornice( current() ) is not { Stands: true }
: string.Equals( current(), style.Name, StringComparison.OrdinalIgnoreCase ),
Choose = style => chosen( style?.Name ?? "" ),
Design = () => new ArchCorniceDesigner( parent, kit, kit.FindCornice( current() ), designed => chosen( designed.Name ) ).Show()
};
var cards = new List<ArchPresetItem<ArchCorniceStyle>> { Bare() };
cards.AddRange( Standing( kit ).Select( style => Card( style, kit ) ) );
browser.Set( cards );
browser.DesignButton.Visible = true;
browser.DesignButton.ToolTip = "Design a cornice style";
return browser;
}
static ArchPresetItem<ArchCorniceStyle> Bare() {
return new ArchPresetItem<ArchCorniceStyle> {
Value = null,
Name = "None",
Detail = "A bare head",
Identity = "cornice.none",
Category = "Plain",
Glyph = "block"
};
}
// The stack an author most likely means: the plainest one that is still a cornice rather than a bare cap.
public static string Offered( ArchKit kit ) {
var wanted = Standing( kit ).FirstOrDefault( style => style.Courses.Any( course => course.Kind == CorniceCourse.Moulding ) )
?? Standing( kit ).FirstOrDefault();
return wanted?.Name ?? "";
}
public static void Build( ToolSidebarWidget panel, ArchKit kit, ArchCorniceStyle style, Action refresh, Action changed ) {
Courses( panel, kit, style, refresh, changed );
Piers( panel, style, refresh, changed );
}
// Stated bottom up, the way it is built and the way a section drawing reads. Order is the list's own, so moving
// a course is moving a row rather than re-typing two heights.
static void Courses( ToolSidebarWidget panel, ArchKit kit, ArchCorniceStyle style, Action refresh, Action changed ) {
var group = panel.AddGroup( $"Courses ({style.Courses.Count}, bottom up)" );
for ( var index = 0; index < style.Courses.Count; index++ ) {
Course( panel, group, kit, style, index, refresh, changed );
}
using var add = ArchIconGrid.In( group );
add.Toggle( "Add a course on top of the stack", "cornice_add", "add", false, _ => {
style.Courses.Add( new ArchCorniceCourse() );
changed?.Invoke();
refresh?.Invoke();
} );
}
static void Course( Widget panel, Layout group, ArchKit kit, ArchCorniceStyle style, int index, Action refresh, Action changed ) {
var course = style.Courses[index];
var seat = index;
using ( var kinds = ArchIconGrid.In( group ) ) {
foreach ( var value in Enum.GetValues<CorniceCourse>() ) {
var wanted = value;
kinds.Pick( Describe( wanted ), $"cornice_{wanted}".ToLowerInvariant(), Glyph( wanted ),
course.Kind == wanted, () => { course.Kind = wanted; changed?.Invoke(); refresh?.Invoke(); } );
}
}
group.Add( ArchPartUi.Number( "Rise", course.Rise, 4f, value => course.Rise = value, changed ) );
group.Add( ArchPartUi.Number( course.Straddles ? "Oversails by" : "Stands proud", course.Projection, 3f,
value => course.Projection = value, changed ) );
if ( course.Kind is CorniceCourse.Moulding or CorniceCourse.Modillions ) {
group.Add( Sections( panel, kit, course, changed ) );
}
if ( course.Kind is CorniceCourse.Dentils or CorniceCourse.Modillions ) {
// A pitch, never a count: the count comes off whatever run the course lands on.
group.Add( ArchPartUi.Number( "Block width", course.Block, kit.DentilWidth, value => course.Block = value, changed ) );
group.Add( ArchPartUi.Number( "Gap beside it", course.Gap, kit.DentilGap, value => course.Gap = value, changed ) );
}
using ( var fields = ArchIconGrid.In( group ) ) {
foreach ( var role in Fields ) {
var wanted = role;
fields.Pick( $"Skinned as {wanted}", $"cornice_surface_{wanted}".ToLowerInvariant(), FieldGlyph( wanted ),
course.Field == wanted, () => { course.Field = wanted; changed?.Invoke(); } );
}
}
using var row = ArchIconGrid.In( group );
row.Toggle( "Move this course down the stack", "cornice_lower", "arrow_downward", false,
_ => Reorder( style, seat, -1, refresh, changed ) );
row.Toggle( "Move this course up the stack", "cornice_raise", "arrow_upward", false,
_ => Reorder( style, seat, 1, refresh, changed ) );
row.Toggle( $"Remove the {course.Kind} course", "cornice_remove", "delete", false, _ => {
style.Courses.RemoveAt( seat );
changed?.Invoke();
refresh?.Invoke();
} );
}
static void Reorder( ArchCorniceStyle style, int index, int step, Action refresh, Action changed ) {
var wanted = index + step;
if ( wanted < 0 || wanted >= style.Courses.Count ) {
return;
}
(style.Courses[index], style.Courses[wanted]) = (style.Courses[wanted], style.Courses[index]);
changed?.Invoke();
refresh?.Invoke();
}
static Widget Sections( Widget parent, ArchKit kit, ArchCorniceCourse course, Action changed ) {
var browser = new ArchPresetBrowser<ArchProfile>( parent, kit, "cornice.sections", "Section" ) {
Chosen = profile => string.Equals( course.Profile, profile.Name, StringComparison.OrdinalIgnoreCase ),
Choose = profile => {
course.Profile = profile.Name;
changed?.Invoke();
}
};
browser.Set( kit.Profiles
.Select( profile => new ArchPresetItem<ArchProfile> {
Value = profile,
Name = profile.Name,
Detail = $"{profile.Max.x - profile.Min.x:0.#} x {profile.Max.y - profile.Min.y:0.#} in",
Identity = profile.Name,
Category = "Section",
Glyph = "show_chart"
} )
.ToList() );
return browser;
}
static void Piers( ToolSidebarWidget panel, ArchCorniceStyle style, Action refresh, Action changed ) {
var pier = style.Pier;
using ( var grid = ArchIconGrid.In( panel.AddGroup( "Piers" ) ) ) {
grid.Toggle( "Break the line above the cornice at every corner", "opt_cornice_piers", "view_column", pier.Stands,
value => { pier.Rise = value ? 16f : 0f; changed?.Invoke(); refresh?.Invoke(); } );
}
if ( !pier.Stands ) {
return;
}
var group = panel.AddGroup( "Pier" );
group.Add( ArchPartUi.Number( "Rises above the head", pier.Rise, 16f, value => pier.Rise = value, changed ) );
group.Add( ArchPartUi.Number( "Width", pier.Width, 16f, value => pier.Width = value, changed ) );
group.Add( ArchPartUi.Number( "Stands proud", pier.Projection, 2f, value => pier.Projection = value, changed ) );
group.Add( ArchPartUi.Number( "Cap height", pier.Cap, 3f, value => pier.Cap = value, changed ) );
group.Add( ArchPartUi.Number( "Cap oversails by", pier.CapOversail, 1.5f, value => pier.CapOversail = value, changed ) );
}
public static string Describe( CorniceCourse kind ) => kind switch {
CorniceCourse.Moulding => "Moulding — a swept section standing proud of the face",
CorniceCourse.Band => "Band — a plain projecting course, the corona",
CorniceCourse.Dentils => "Dentils — a row of small blocks",
CorniceCourse.Modillions => "Modillions — the same row, cut from a section",
_ => "Coping — a cap straddling the wall, oversailing both faces"
};
static string Glyph( CorniceCourse kind ) => kind switch {
CorniceCourse.Moulding => "waves",
CorniceCourse.Band => "horizontal_rule",
CorniceCourse.Dentils => "view_week",
CorniceCourse.Modillions => "view_column",
_ => "border_top"
};
static string FieldGlyph( ArchSurface field ) => field switch {
ArchSurface.WallCap => "border_top",
ArchSurface.WallExterior => "villa",
ArchSurface.Pillar => "view_column",
ArchSurface.WallBase => "grid_4x4",
ArchSurface.Panel => "crop_square",
_ => "horizontal_rule"
};
}