Editor/Designers/ArchBuildingDesigner.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// Authors a building TYPE only - nothing placed on it afterwards is staged here.
public sealed class ArchBuildingDesigner : ArchDesignerDialog {
readonly ArchTool tool;
readonly Action<ArchArchetype> applied;
readonly ArchArchetype draft;
Vector2 plot = new( 480f, 312f );
RoofStyle roof = RoofStyle.Flat;
bool withRoof = true;
public ArchBuildingDesigner( Widget parent, ArchTool tool, ArchArchetype initial, Action<ArchArchetype> applied )
: base( parent, "Building Designer", "domain" ) {
this.tool = tool;
this.applied = applied;
draft = Copy( initial );
if ( draft.Shell.Roof is { } style ) {
roof = style;
}
Assemble(
tool.Kit,
"Live generated building",
"Drag the preview to orbit. A whole unit through the same shell, wall and roof generators the plan builds with - what you see is what a drag will stand.",
$"Building types: Assets/{ArchStorage.ArchetypeDirectory}" );
}
protected override string SaveLabel => "Save Type";
protected override void Populate() {
var form = Form.Layout;
form.Add( Heading( "Identity" ) );
form.Add( Text( "Name", draft.Name, value => draft.Name = value ) );
form.Add( Text( "Title", draft.Title, value => draft.Title = value ) );
form.Add( Text( "Description", draft.Description, value => draft.Description = value ) );
form.Add( Heading( "Preview plot" ) );
form.Add( Number( "Along x", plot.x, value => plot = plot.WithX( MathF.Max( 96f, value ) ) ) );
form.Add( Number( "Along y", plot.y, value => plot = plot.WithY( MathF.Max( 96f, value ) ) ) );
var options = new ToolSidebarWidget();
Shell( options );
Walls( options );
Roof( options );
form.Add( options );
}
void Shell( ToolSidebarWidget panel ) {
var group = panel.AddGroup( "Shell" );
group.Add( ArchPartUi.Number( "Wall height", draft.Shell.WallHeight, tool.Kit.WallHeight,
value => draft.Shell.WallHeight = value, Stage.Restage ) );
using var grid = ArchIconGrid.In( group );
grid.Toggle( "Floor slab", "opt_floor_slab", "layers", draft.Shell.Floor ?? true,
value => { draft.Shell.Floor = value; Stage.Restage(); } );
grid.Toggle( "Stands on a raised foundation", "opt_raised", "foundation", draft.Shell.Foundation ?? true,
value => { draft.Shell.Foundation = value; Stage.Restage(); } );
grid.Toggle( "Ceiling under the roof", "opt_ceiling", "square", draft.Shell.Ceiling ?? false,
value => { draft.Shell.Ceiling = value; Stage.Restage(); } );
}
void Walls( ToolSidebarWidget panel ) {
draft.Shell.Wall ??= new ArchWallPreset();
using ( var grid = ArchIconGrid.In( panel.AddGroup( "Treatment" ) ) ) {
foreach ( var preset in tool.Kit.Walls ) {
var captured = preset;
grid.Pick( captured.Name, ArchIcons.WallSlug( captured.Name ), "villa",
string.Equals( draft.Shell.Wall.Name, captured.Name, StringComparison.OrdinalIgnoreCase ),
() => {
draft.Shell.Wall = ArchWallPreset.Copy( captured );
Repopulate();
} );
}
}
ArchWallForms.Wall( panel, tool.Kit, draft.Shell.Wall, Repopulate, Stage.Restage );
}
void Roof( ToolSidebarWidget panel ) {
using ( var grid = ArchIconGrid.In( panel.AddGroup( "Roof" ) ) ) {
grid.Toggle( "Roof over it", "opt_roof", "roofing", withRoof, value => { withRoof = value; Repopulate(); } );
}
if ( !withRoof ) {
return;
}
using ( var grid = ArchIconGrid.In( panel.AddGroup( "Roof style" ) ) ) {
foreach ( var value in Enum.GetValues<RoofStyle>() ) {
var captured = value;
grid.Pick( captured.ToString(), ArchIcons.RoofStyleSlug( captured ), ArchIcons.RoofStyleGlyph( captured ),
roof == captured, () => { roof = captured; draft.Shell.Roof = captured; Repopulate(); } );
}
}
var group = panel.AddGroup( "Deck" );
group.Add( ArchPartUi.Number( "Pitch", draft.Shell.RoofPitch, tool.Kit.RoofPitch,
value => draft.Shell.RoofPitch = value, Stage.Restage ) );
group.Add( ArchPartUi.Number( "Overhang", draft.Shell.Overhang, 16f, value => draft.Shell.Overhang = value, Stage.Restage ) );
using ( var grid = ArchIconGrid.In( group ) ) {
grid.Toggle( "Parapet standing past the deck with a coping on it", "opt_parapet", "crop_din", draft.Shell.Parapet ?? false,
value => { draft.Shell.Parapet = value; Repopulate(); } );
grid.Toggle( "Gutters and downpipes", "opt_gutters", "water_damage", draft.Shell.Gutters ?? true,
value => { draft.Shell.Gutters = value; Stage.Restage(); } );
grid.Toggle( "Fascia along the eaves", "opt_fascia", "border_bottom", draft.Shell.Fascia ?? true,
value => { draft.Shell.Fascia = value; Stage.Restage(); } );
grid.Toggle( "Lined soffit under the overhang", "opt_soffit", "flip_to_back", draft.Shell.Soffit ?? true,
value => { draft.Shell.Soffit = value; Stage.Restage(); } );
grid.Toggle( "Exposed rafters and purlins under the deck", "opt_roof_frame", "reorder", draft.Shell.Frame ?? false,
value => { draft.Shell.Frame = value; Stage.Restage(); } );
}
if ( draft.Shell.Parapet == true ) {
group.Add( ArchPartUi.Number( "Parapet height", draft.Shell.ParapetHeight, 18f,
value => draft.Shell.ParapetHeight = value, Stage.Restage ) );
}
}
protected override ArchStaged Staged() => ArchArchetypeStage.Of( Snapshot(), tool.Kit, plot, withRoof ? roof : null );
protected override string Diagnose() {
var recipe = Snapshot();
var wall = recipe.Shell.Wall;
var staged = Staged().Plan;
var room = staged?.Buildings.FirstOrDefault()?.Rooms.FirstOrDefault();
return string.Join( "\n",
$"type: {recipe.Name} ({recipe.Title})",
$"plot: {plot.x:0.##} x {plot.y:0.##}, wall height {ArchArchetypeRules.Standing( recipe.Shell, tool.Kit ):0.##}",
$"shell: {room?.Walls.Count ?? 0} walls, floor {recipe.Shell.Floor}, foundation {recipe.Shell.Foundation}, ceiling {recipe.Shell.Ceiling}",
$"roof: {(withRoof ? roof.ToString() : "none")} pitch {recipe.Shell.RoofPitch:0.##} overhang {recipe.Shell.Overhang:0.##}, "
+ $"parapet {recipe.Shell.Parapet} at {recipe.Shell.ParapetHeight:0.##}, frame {recipe.Shell.Frame}",
$"walls: cladding {wall?.Cladding}, {wall?.Bands.Count ?? 0} bands "
+ $"[{string.Join( " ", ArchWallTreatments.Ordered( wall?.Bands, room?.WallHeight ?? tool.Kit.WallHeight )
.Select( band => $"{(band.AtHead ? "head" : band.Height.ToString( "0.#" ))}:{band.Field}" ) )}]",
$"pilasters: " + (wall?.Pilasters.Stands == true
? $"{wall.Pilasters.Bay:0.##} bay, {ArchWallSection.PilasterCentres( wall.Pilasters, 0f, plot.x ).Count()} strips over {plot.x:0.##}"
: "none") );
}
protected override void Apply() {
applied?.Invoke( Snapshot() );
Close();
}
protected override void SaveTemplate() {
var saved = Snapshot();
if ( string.IsNullOrWhiteSpace( saved.Name ) ) {
Log.Warning( "Architecture: the building type needs a name before it can be saved." );
return;
}
if ( !ArchStorage.SaveArchetype( saved ) ) {
Log.Warning( $"Architecture: could not write the building type '{saved.Name}'." );
return;
}
tool.ReloadArchetypes();
applied?.Invoke( saved );
Close();
}
ArchArchetype Snapshot() {
var result = Copy( draft );
result.Name = string.IsNullOrWhiteSpace( draft.Name ) ? "building" : draft.Name.Trim();
result.Title = string.IsNullOrWhiteSpace( draft.Title ) ? result.Name : draft.Title.Trim();
result.Shell.Roof = withRoof ? roof : null;
// The wing inherits the shell's treatment so an extension matches; its roof stays as authored.
result.Wing.Wall = result.Shell.Wall is null ? null : ArchWallPreset.Copy( result.Shell.Wall );
result.Wing.WallHeight = result.Shell.WallHeight;
result.Wing.Parapet = result.Shell.Parapet;
result.Wing.ParapetHeight = result.Shell.ParapetHeight;
result.Wing.Fascia = result.Shell.Fascia;
result.Wing.Soffit = result.Shell.Soffit;
result.Wing.Frame = result.Shell.Frame;
result.Wing.Gutters = result.Shell.Gutters;
return result;
}
// Deep copy: a cancelled designer must leave the on-disk type exactly as it was.
public static ArchArchetype Copy( ArchArchetype type ) {
type ??= new ArchArchetype();
return new ArchArchetype {
Version = type.Version,
Name = type.Name,
Title = type.Title,
Description = type.Description,
Icon = type.Icon,
Openings = new List<string>( type.Openings ),
Pillars = new List<string>( type.Pillars ),
Shell = Copy( type.Shell ),
Wing = Copy( type.Wing ),
WingRoof = type.WingRoof,
WingEaveDrop = type.WingEaveDrop,
Shows = new List<ArchOptionGroup>( type.Shows ),
DriveWidth = type.DriveWidth,
DriveSplay = type.DriveSplay,
DriveKind = type.DriveKind,
MinimumPlot = type.MinimumPlot,
Steps = type.Steps
};
}
static ArchSectionRules Copy( ArchSectionRules rules ) {
rules ??= new ArchSectionRules();
var result = new ArchSectionRules {
WallHeight = rules.WallHeight,
Roof = rules.Roof,
Ridge = rules.Ridge,
RoofPitch = rules.RoofPitch,
Overhang = rules.Overhang,
Floor = rules.Floor,
Foundation = rules.Foundation,
Ceiling = rules.Ceiling,
Gutters = rules.Gutters,
Fascia = rules.Fascia,
Soffit = rules.Soffit,
Frame = rules.Frame,
FrameSpacing = rules.FrameSpacing,
FloorBoards = rules.FloorBoards,
FloorBoardYaw = rules.FloorBoardYaw,
Wall = rules.Wall is null ? null : ArchWallPreset.Copy( rules.Wall ),
Parapet = rules.Parapet,
ParapetHeight = rules.ParapetHeight
};
ArchArchetypeRules.Skin( result.Palette, rules.Palette );
return result;
}
}