Editor/Tool/Subtools/ArchBuildingSubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
public enum BuildingMode {
New,
Extend,
Prefab,
Row,
Saved
}
[Title( "Building" ), Icon( "domain_add" ), Group( "02" )]
public sealed class ArchBuildingSubtool( ArchTool owner ) : ArchSubtool( owner ) {
protected override ArchKind? DraftKind => ArchKind.Building;
public override ArchSurface[] Surfaces => new[] { ArchSurface.WallExterior, ArchSurface.WallInterior, ArchSurface.Floor, ArchSurface.Roof, ArchSurface.Soffit, ArchSurface.WallCap };
BuildingMode mode;
string archetype = "house";
bool flipX;
bool flipY;
bool withRoof = true;
bool withFloor = true;
bool withGutters = true;
bool withFoundation = true;
RoofStyle roofStyle = RoofStyle.Hip;
RidgeRun ridge = RidgeRun.Auto;
SectionRoof wingRoof = SectionRoof.Continue;
float eaveDrop;
float wallHeight;
float wallThickness;
float roofThickness;
float roofPitch;
bool withFrame;
bool withFascia = true;
bool withSoffit = true;
bool withCeiling;
bool seeded;
ArchBuildingAsset saved;
ArchAssetGhost savedGhost;
List<ArchPresetItem<ArchBuildingAsset>> savedOffered;
int savedStamp = -1;
int quarters;
float placementAngle;
int placedRoom;
int placedRoof;
int placedStamp;
bool placedMerged;
bool Extending => mode == BuildingMode.Extend;
bool Rowing => mode == BuildingMode.Row;
bool Stamping => mode == BuildingMode.Saved;
protected override bool UsesDrag => !Stamping;
ArchArchetype Chosen => Owner.FindArchetype( archetype );
float Facing => placementAngle + quarters * 90f;
bool Aimable => !Extending && !Rowing;
protected override string Title() => Stamping ? "Saved Building" : "Building";
bool Roofed => Extending ? wingRoof != SectionRoof.None : withRoof;
protected override string Advice() => mode switch {
BuildingMode.Extend => "Drag a wing onto the active building. It shares the wall it abuts, and inherits its type.",
BuildingMode.Prefab => "Drag the plot. The type's canned layout is laid out across it in one go.",
BuildingMode.Row => "Drag the frontage and how far back it reaches. Along a street it takes that road's verge and the units step with the curve.",
BuildingMode.Saved => "Pick a saved building and it follows the cursor. R turns it a quarter, the Bearing box aims it at anything else; click stands it there.",
_ => "Drag the outer shell. The chosen type decides its heights, roof and trims."
};
public override void OnEnabled() {
base.OnEnabled();
ArchWorkflow.Restart( Scope( "flow" ) );
}
public override void OnUpdate() {
base.OnUpdate();
if ( Stamping && savedOffered is not null && savedStamp != ArchLayerAssets.Revision ) {
Refresh();
}
}
public override bool Turns() {
if ( !Stamping || !Placing || saved is null ) {
return false;
}
quarters = (quarters + 1) % 4;
return true;
}
protected override void DrawHover( Vector2 point ) {
base.DrawHover( point );
if ( !Stamping || savedGhost is not { Shapes.Count: > 0 } ghost ) {
return;
}
var map = ArchBuildingStamp.Landing( ghost, point, Facing );
foreach ( var shape in ghost.Shapes ) {
ArchGhost.Prism( shape.Loop.Select( corner => map.Of( corner ) ).ToList(), shape.Bottom, shape.Top );
}
var span = map.Swapped( ghost.Max - ghost.Min );
ArchGhost.Note( new Vector3( point.x + span.x, point.y + span.y, Owner.LevelHeight ),
$"{saved.Title} — {span.x:0} x {span.y:0}, turned {Facing:0.##}°" );
}
protected override void OnClick( Vector2 point ) {
if ( !Stamping ) {
return;
}
if ( saved is null ) {
Log.Info( "Architecture: pick a saved building before clicking - there is nothing to stand yet." );
return;
}
if ( ArchBuildingStamp.Place( Owner.Plan, saved, point, Facing ) is not { } placed ) {
Log.Info( $"Architecture: {saved.Title} could not be stamped there." );
return;
}
Owner.ActiveBuildingId = placed.Id;
Owner.ActiveRoomId = placed.Rooms[0].Id;
FinishDraft( placed.Id );
BeginDraft();
Owner.CommitPlacement( $"Place {saved.Title}", placed );
}
protected override void DrawPreview() {
if ( mode == BuildingMode.Prefab ) {
DrawPrefabPreview();
return;
}
if ( Rowing ) {
DrawRowPreview();
return;
}
var standard = ArchArchetypeRules.Standing( wallHeight, Owner.Kit );
var height = Extending ? MathF.Max( 32f, standard - MathF.Max( 0f, eaveDrop ) ) : standard;
var placement = Resolve( DragStart, DragCurrent );
if ( !placement.IsUsable || Extending && !placement.TouchesHost ) {
DrawRectPreview();
return;
}
var aimed = Aimable && MathF.Abs( Facing ) > 0.001f;
if ( aimed ) {
ArchGhost.Prism( Turned( placement.Min, placement.Max ), Owner.LevelHeight, Owner.LevelHeight + height );
} else {
ArchGhost.Volume( placement.Min, placement.Max, Owner.LevelHeight, Owner.LevelHeight + height );
}
ArchGhost.Note( new Vector3( placement.Max.x, placement.Max.y, Owner.LevelHeight + height ),
aimed
? $"{placement.Max.x - placement.Min.x:0} x {placement.Max.y - placement.Min.y:0}, turned {Facing:0.##}°"
: $"{placement.Max.x - placement.Min.x:0} x {placement.Max.y - placement.Min.y:0}" );
if ( !withRoof && !Extending || aimed ) {
return;
}
var min = placement.Min;
var max = placement.Max;
var style = Extending ? ArchBuild.Winged( wingRoof ) : roofStyle;
var pitch = roofPitch > 0.5f ? roofPitch : Owner.Kit.RoofPitch;
ArchGhost.Pitch( min, max, Owner.LevelHeight + height, pitch, style, ArchBuild.Ridged( ridge, min, max ) );
}
void DrawPrefabPreview() {
var recipe = Owner.FindArchetype( archetype );
if ( recipe is null || !recipe.HasLayout ) {
DrawRectPreview();
return;
}
var plot = ResolvedPrefabPlot( recipe, DragStart, DragCurrent, out var usable );
if ( !usable ) {
DrawRectPreview();
return;
}
ArchGhost.Plate( plot.Min, plot.Max, Owner.LevelHeight, 8 );
foreach ( var step in recipe.Steps ) {
if ( step.Kind is not (ArchStepKind.Shell or ArchStepKind.Wing or ArchStepKind.Canopy) ) {
continue;
}
plot.Resolve( step.Rect, out var min, out var max );
ArchGhost.Volume( min, max, Owner.LevelHeight, Owner.LevelHeight + Standing( recipe, step ) );
}
ArchGhost.Note( new Vector3( plot.Max.x, plot.Max.y, Owner.LevelHeight + 264f ),
$"{recipe.Title} — {plot.Size.x:0} x {plot.Size.y:0}" );
}
void DrawRowPreview() {
var row = ResolvedRow( DragStart, DragCurrent );
if ( !row.IsUsable ) {
DrawRectPreview();
return;
}
var standing = ArchArchetypeRules.Standing( wallHeight, Owner.Kit );
var storey = standing + Owner.Kit.FloorThickness;
var pitch = roofPitch > 0.5f ? roofPitch : Owner.Kit.RoofPitch;
foreach ( var bay in row.Bays ) {
var plate = Owner.LevelHeight + standing + storey * (bay.Storeys - 1);
ArchGhost.Volume( bay.Min, bay.Max, Owner.LevelHeight, plate );
if ( withRoof ) {
ArchGhost.Pitch( bay.Min, bay.Max, plate, pitch, roofStyle, ArchBuild.Ridged( ridge, bay.Min, bay.Max ) );
}
}
var last = row.Bays[^1];
ArchGhost.Note( new Vector3( last.Max.x, last.Max.y, Owner.LevelHeight + standing ),
row.RoadId > 0
? $"{row.Bays.Count} units on the verge — {row.Frontage:0} x {row.Depth:0}"
: $"{row.Bays.Count} units — {row.Frontage:0} x {row.Depth:0}" );
}
protected override void OnDrag( Vector2 from, Vector2 to ) {
if ( mode == BuildingMode.Prefab ) {
PlacePrefab( from, to );
return;
}
if ( Rowing ) {
PlaceRow( from, to );
return;
}
var placement = Resolve( from, to );
var min = placement.Min;
var max = placement.Max;
if ( !placement.IsUsable || Extending && !placement.TouchesHost ) {
Log.Info( Extending
? "Architecture: an extension must finish against an empty edge of the active building."
: "Architecture: that drag is fully occupied. Start or finish the drag in empty grid space." );
return;
}
var rules = Authored();
if ( Extending && placement.Host is { Rooms.Count: > 0 } active ) {
active.Archetype = Chosen?.Name ?? active.Archetype;
Owner.ActiveBuildingId = active.Id;
var wing = ArchBuild.Extend(
Owner.Plan, active, Owner.Kit, Owner.Level, Owner.LevelHeight, min, max,
wingRoof, eaveDrop, rules.Floor ?? true, rules.Gutters ?? true, ridge );
if ( wing is null ) {
Log.Info( "Architecture: that wing has no empty grid space beside the building." );
return;
}
ArchArchetypeRules.Apply( wing, rules, Owner.Kit );
Owner.ActiveRoomId = wing.Room.Id;
CancelDraft();
Owner.CommitPlacement( "Place Building Wing", active );
Remember( wing );
return;
}
var building = new ArchBuilding {
Id = Owner.Plan.AllocateId(),
Name = $"{Chosen?.Title ?? "Building"}{Owner.Plan.Buildings.Count + 1}",
Archetype = Chosen?.Name ?? "",
GuttersEnabled = rules.Gutters ?? true
};
var shell = ArchBuild.Shell(
Owner.Plan, building, Owner.Kit, Owner.Level, Owner.LevelHeight, min, max, rules.Floor ?? true,
withRoof ? roofStyle : null, rules.Gutters ?? true, ridge );
if ( shell is null ) {
Log.Info( "Architecture: that building footprint has no empty grid space." );
return;
}
ArchArchetypeRules.Apply( shell, rules, Owner.Kit );
Owner.Plan.Units.Add( building );
if ( Aimable && ArchHandles.Pivot( building, out var about ) ) {
ArchCarry.Turn( building, about, Facing );
}
Owner.ActiveBuildingId = building.Id;
Owner.ActiveRoomId = shell.Room.Id;
FinishDraft( building.Id );
Owner.CommitPlacement( "Place Building", building );
Remember( shell );
}
static List<Vector2> Turned( Vector2 min, Vector2 max, float degrees ) => ArchFootprint.Turned( ArchFootprint.Rect( min, max ), (min + max) * 0.5f, degrees );
List<Vector2> Turned( Vector2 min, Vector2 max ) => Turned( min, max, Facing );
ArchRectanglePlacement Resolve( Vector2 from, Vector2 to ) {
return new ArchBoundaryPlacement( Owner.Plan, Owner.Kit )
.Outside( Owner.Level, from, to, Extending ? Owner.ActiveBuilding() : null );
}
void Remember( ArchSection section ) {
placedRoom = section.Room?.Id ?? 0;
placedRoof = section.Roof?.Id ?? 0;
placedStamp = Owner.Plan.NextId;
placedMerged = section.Merged;
Refresh();
}
ArchRoom Editing => placedRoom != 0 && Owner.Plan.NextId == placedStamp ? Owner.Plan.FindRoom( placedRoom ) : null;
void Restyle() {
if ( placedRoom == 0 || Owner.Plan.NextId != placedStamp ) {
return;
}
if ( Owner.Plan.FindRoom( placedRoom ) is not { } room ) {
placedRoom = 0;
return;
}
var building = Owner.Plan.OwnerOf( room );
var roof = building?.Roofs.FirstOrDefault( part => part.Id == placedRoof );
if ( building is not null && Chosen is { } chosen ) {
building.Archetype = chosen.Name;
}
if ( roof is not null && !placedMerged ) {
roof.Style = Extending ? ArchBuild.Winged( wingRoof ) : roofStyle;
roof.RidgeAlongX = ArchBuild.Ridged( ridge, roof.Min, roof.Max );
}
ArchArchetypeRules.Apply( new ArchSection { Room = room, Roof = roof, Merged = placedMerged }, Authored(), Owner.Kit );
Owner.Touch( "Restyle Section" );
}
void Set( Action change ) {
change();
Restyle();
}
void Relayout( Action change ) {
Set( change );
Refresh();
}
ArchSectionRules Authored() {
var type = Chosen is { } chosen ? (Extending ? chosen.Wing : chosen.Shell) : new ArchSectionRules();
return new ArchSectionRules {
WallHeight = wallHeight,
WallThickness = wallThickness,
RoofThickness = roofThickness,
Ridge = ridge,
RoofPitch = roofPitch,
Overhang = type.Overhang,
FrameSpacing = type.FrameSpacing,
Floor = withFloor,
Foundation = withFoundation,
Ceiling = withCeiling,
Gutters = withGutters,
Fascia = withFascia,
Soffit = withSoffit,
Frame = withFrame,
FloorBoards = type.FloorBoards,
FloorBoardYaw = type.FloorBoardYaw,
Palette = type.Palette
};
}
float Standing( ArchArchetype recipe, ArchArchetypeStep step ) {
if ( step.Kind == ArchStepKind.Canopy ) {
return step.HeadHeight;
}
var type = step.Kind == ArchStepKind.Wing ? recipe.Wing : recipe.Shell;
var height = step.Rules.WallHeight > 1f ? step.Rules.WallHeight : type.WallHeight;
return ArchArchetypeRules.Standing( height, Owner.Kit );
}
void PlacePrefab( Vector2 from, Vector2 to ) {
var recipe = Owner.FindArchetype( archetype );
if ( recipe is null || !recipe.HasLayout ) {
Log.Info( $"Architecture: '{archetype}' has no canned layout - use New with the type chosen instead. Types live in Assets/{ArchStorage.ArchetypeDirectory}." );
return;
}
var plot = ResolvedPrefabPlot( recipe, from, to, out var usable );
if ( !usable ) {
Log.Info( $"Architecture: {recipe.Title} has no empty grid space on that plot." );
return;
}
var placed = ArchArchetypeBuild.Place( Owner.Plan, Owner.Kit, recipe, plot, Owner.Level, Owner.LevelHeight, Owner.PillarTypes );
if ( placed is null ) {
Log.Info( $"Architecture: {recipe.Title} laid out nothing on that plot." );
return;
}
foreach ( var note in placed.Skipped ) {
Log.Info( $"Architecture: {recipe.Title} skipped {note}." );
}
Owner.ActiveBuildingId = placed.Building.Id;
Owner.ActiveRoomId = placed.Building.Rooms[0].Id;
Owner.CommitPlacement( $"Place {recipe.Title}", placed.Building );
}
ArchPlot ResolvedPrefabPlot( ArchArchetype recipe, Vector2 from, Vector2 to, out bool usable ) {
return new ArchBoundaryPlacement( Owner.Plan, Owner.Kit )
.PlotOutside( Owner.Level, from, to, recipe.MinimumPlot, flipX, flipY, out usable );
}
void PlaceRow( Vector2 from, Vector2 to ) {
if ( Chosen is not { } type ) {
Log.Info( $"Architecture: a row takes its unit width from a building type, and none are in Assets/{ArchStorage.ArchetypeDirectory}." );
return;
}
var row = ResolvedRow( from, to );
if ( !row.IsUsable ) {
Log.Info( $"Architecture: that drag names no frontage a {type.Title} unit fits along. Drag further along the street, or deeper back from it." );
return;
}
var placed = ArchRowPlacement.Stand(
Owner.Plan, Owner.Kit, row, type, Authored(), Owner.Level, Owner.LevelHeight,
withRoof ? roofStyle : null, ridge );
if ( placed is null ) {
Log.Info( "Architecture: every bay of that row landed on occupied ground." );
return;
}
if ( row.Blocked > 0 ) {
Log.Info( $"Architecture: {row.Blocked} of the row's bays stood on occupied ground and were left out." );
}
Owner.ActiveBuildingId = placed.Buildings[0].Id;
Owner.ActiveRoomId = placed.Buildings[0].Rooms[0].Id;
CancelDraft();
Owner.CommitPlacement( $"Place Street Row of {placed.Buildings.Count}", placed.Buildings.ToArray() );
}
ArchRowShape ResolvedRow( Vector2 from, Vector2 to ) {
var road = Owner.RoadAt( from, ArchRowPlacement.Verge, out _ ) ?? Owner.RoadAt( to, ArchRowPlacement.Verge, out _ );
return ArchRowPlacement.Resolve(
Owner.Plan, Owner.Kit, Chosen, Owner.Level, from, to,
road, road is null ? null : Owner.Resolved( road.Id, road.Curve ),
withRoof && roofStyle == RoofStyle.Flat );
}
void Layout( ToolSidebarWidget panel ) {
var building = Owner.ActiveBuilding();
var button = new Button.Primary( "Toggle Building Layout", "architecture" ) {
Clicked = () => ArchBuildingWindow.Toggle( Owner, building )
};
button.Enabled = building is not null;
button.ToolTip = building is null
? "Drag a shell first — the layout drawing works on a building that stands"
: $"The plan of {building.Name}, storey by storey: move a corner, push a run, cut a corner off at an angle";
panel.AddGroup( "Layout" ).AddRow().Add( button );
}
protected override void BuildOptions( ToolSidebarWidget panel ) {
Layout( panel );
Inherit();
if ( Stamping && (savedOffered is null || savedStamp != ArchLayerAssets.Revision) ) {
RereadSaved();
}
if ( Editing is { } room ) {
panel.Layout.Add( ArchSidebarLayout.Advice( $"Editing {room.Name} live. Place anything else and these go back to seeding the next drag." ) );
}
using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );
flow.Step( "Mode", ModeName, ModeGlyph, PickMode );
if ( Stamping ) {
flow.Step( "Saved building", saved?.Title ?? "None", "inventory_2", step => Stamps( panel, step ) );
} else {
flow.Step( "Type", Chosen?.Title ?? "None", "domain", step => Types( panel, step ) );
}
if ( mode == BuildingMode.Prefab ) {
flow.Step( "Mirror", MirrorName, "flip", PickMirror );
} else if ( !Stamping ) {
flow.Step( "Include", IncludeName, "checklist", PickInclude );
if ( Covered ) {
flow.Step( Extending ? "Wing roof" : "Roof", RoofName, RoofGlyph, PickRoof );
}
if ( Wants( ArchOptionGroup.Section ) ) {
flow.Step( "Structure", StructureName, "straighten", BuildStructure );
}
}
// Typed, not picked — can never auto-advance; must be last
if ( Aimable ) {
flow.Step( "Bearing", BearingName, "explore", BuildBearing );
}
}
bool Covered => Extending ? Wants( ArchOptionGroup.WingRoof ) : withRoof;
bool Wants( ArchOptionGroup group ) => Chosen?.Wants( group ) ?? true;
string ModeName => mode switch {
BuildingMode.Extend => "Extend",
BuildingMode.Prefab => "Canned layout",
BuildingMode.Row => "Street row",
BuildingMode.Saved => "Saved building",
_ => "New building"
};
string ModeGlyph => mode switch {
BuildingMode.Extend => "add_home_work",
BuildingMode.Prefab => "auto_awesome_motion",
BuildingMode.Row => "view_column",
BuildingMode.Saved => "inventory_2",
_ => "domain_add"
};
void PickMode( ArchWorkflowStep step ) {
using var grid = ArchIconGrid.In( step.Layout );
Mode( grid, step, BuildingMode.New, "mode_new_building", "New building — drag the outer shell", "domain_add" );
Mode( grid, step, BuildingMode.Extend, "mode_extend_building", "Extend the active building — drag a wing onto it", "add_home_work" );
Mode( grid, step, BuildingMode.Prefab, "mode_archetype", "Place the type's canned layout — one drag lays the whole unit out", "auto_awesome_motion" );
Mode( grid, step, BuildingMode.Row, "mode_street_row", "Street row — drag the frontage and its depth; one party-walled unit per bay, at the type's own unit width", "view_column" );
Mode( grid, step, BuildingMode.Saved, "mode_saved_building", "Saved building — stamp one you authored earlier back down, exactly as it stands", "inventory_2" );
}
void Mode( ArchIconGrid grid, ArchWorkflowStep step, BuildingMode choice, string slug, string tooltip, string fallback ) {
grid.Pick( tooltip, slug, fallback, mode == choice, () => {
mode = choice;
seeded = false;
step.Chose();
} );
}
string MirrorName => flipX && flipY ? "X and Y" : flipX ? "Across X" : flipY ? "Across Y" : "None";
void PickMirror( ArchWorkflowStep step ) {
using var grid = ArchIconGrid.In( step.Layout );
grid.Toggle( "Mirror the layout across X", "flip_x", "swap_horiz", flipX, value => flipX = value );
grid.Toggle( "Mirror the layout across Y", "flip_y", "swap_vert", flipY, value => flipY = value );
}
string IncludeName {
get {
var carried = new List<string>();
if ( Wants( ArchOptionGroup.Foundation ) ) {
carried.Add( withFoundation ? "Raised" : "Nested" );
}
if ( withFloor ) {
carried.Add( "floor" );
}
if ( Roofed && withGutters ) {
carried.Add( "gutters" );
}
if ( !Extending && !withRoof ) {
carried.Add( "no roof" );
}
return carried.Count > 0 ? string.Join( ", ", carried ) : "Shell only";
}
}
void PickInclude( ArchWorkflowStep step ) {
if ( Wants( ArchOptionGroup.Foundation ) ) {
using var footing = ArchIconGrid.In( step.Layout );
footing.Pick( "Raised foundation — the plinth stands proud of grade and the building sits up on it",
"foundation_raised", "vertical_align_top", withFoundation, () => Answer( () => withFoundation = true, step ) );
footing.Pick( "Nested foundation — the footing is buried and the floor sits on grade",
"foundation_nested", "vertical_align_bottom", !withFoundation, () => Answer( () => withFoundation = false, step ) );
}
using var grid = ArchIconGrid.In( step.Layout );
grid.Toggle( "Floor slab", "opt_floor_slab", "layers", withFloor, value => Set( () => withFloor = value ) );
if ( Roofed ) {
grid.Toggle( "Gutters and downpipes", "opt_gutters", "water_damage", withGutters, value => Set( () => withGutters = value ) );
}
if ( !Extending ) {
grid.Toggle( "Roof", "opt_roof", "roofing", withRoof, value => Relayout( () => withRoof = value ) );
}
}
string RoofName => Extending ? wingRoof.ToString() : roofStyle.ToString();
string RoofGlyph => Extending ? Fallback( wingRoof ) : ArchIcons.RoofStyleGlyph( roofStyle );
void PickRoof( ArchWorkflowStep step ) {
if ( Extending ) {
PickWingRoof( step );
return;
}
if ( Wants( ArchOptionGroup.RoofStyle ) ) {
using var grid = ArchIconGrid.In( step.Layout );
foreach ( var value in Enum.GetValues<RoofStyle>() ) {
var captured = value;
grid.Pick( captured.ToString(), ArchIcons.RoofStyleSlug( captured ), ArchIcons.RoofStyleGlyph( captured ), roofStyle == captured,
() => Answer( () => roofStyle = captured, step ) );
}
}
Ridge( step.Layout );
}
void PickWingRoof( ArchWorkflowStep step ) {
using ( var grid = ArchIconGrid.In( step.Layout ) ) {
foreach ( var value in Enum.GetValues<SectionRoof>() ) {
var captured = value;
grid.Pick( Describe( captured ), $"wing_{captured}".ToLowerInvariant(), Fallback( captured ), wingRoof == captured,
() => Answer( () => wingRoof = captured, step ) );
}
}
if ( wingRoof is not (SectionRoof.Continue or SectionRoof.None) ) {
step.Layout.Add( ArchPartUi.Number( "Eave drop", eaveDrop, 0f, value => Set( () => eaveDrop = value ) ) );
}
Ridge( step.Layout );
}
void Answer( Action change, ArchWorkflowStep step ) {
Set( change );
step.Chose();
}
string StructureName => $"{ArchArchetypeRules.Standing( wallHeight, Owner.Kit ):0} high";
void BuildStructure( ArchWorkflowStep step ) {
step.Layout.Add( ArchPartUi.Number( "Wall height", wallHeight, 0f, value => Set( () => wallHeight = value ) ) );
var shownWall = wallThickness > 0f ? wallThickness : Editing?.Walls.FirstOrDefault()?.Thickness ?? Owner.Kit.WallThickness;
step.Layout.Add( ArchPartUi.Number( "Wall thickness", shownWall > 0f ? shownWall : Owner.Kit.WallThickness, Owner.Kit.WallThickness,
value => Set( () => wallThickness = MathF.Max( 1f, value ) ) ) );
if ( Roofed ) {
var style = Extending ? ArchBuild.Winged( wingRoof ) : roofStyle;
if ( style != RoofStyle.Flat )
{
step.Layout.Add( ArchPartUi.Number( "Roof pitch", roofPitch, 0f, value => Set( () => roofPitch = value ) ) );
}
var placed = Editing is { } room ? Owner.Plan.OwnerOf( room )?.Roofs.FirstOrDefault( part => part.Id == placedRoof ) : null;
var shownRoof = roofThickness > 0f ? roofThickness : placed?.Thickness ?? Owner.Kit.RoofThickness;
step.Layout.Add( ArchPartUi.Number( style == RoofStyle.Flat ? "Slab thickness" : "Roof thickness", shownRoof > 0f ? shownRoof : Owner.Kit.RoofThickness, Owner.Kit.RoofThickness,
value => Set( () => roofThickness = MathF.Max( 1f, value ) ) ) );
}
using var grid = ArchIconGrid.In( step.Layout );
grid.Toggle( "Ceiling under the roof", "opt_ceiling", "square", withCeiling, value => Set( () => withCeiling = value ) );
if ( !Roofed ) {
return;
}
grid.Toggle( "Exposed rafters and purlins under the deck", "opt_roof_frame", "reorder", withFrame, value => Set( () => withFrame = value ) );
grid.Toggle( "Fascia along the eaves", "opt_fascia", "border_bottom", withFascia, value => Set( () => withFascia = value ) );
grid.Toggle( "Lined soffit under the overhang", "opt_soffit", "flip_to_back", withSoffit, value => Set( () => withSoffit = value ) );
}
string BearingName => MathF.Abs( Facing ) < 0.001f ? "Square on" : $"{Facing:0.##}°";
void BuildBearing( ArchWorkflowStep step ) {
step.Layout.Add( ArchPartUi.Angle( "Angle", placementAngle, value => placementAngle = value ) );
step.Layout.Add( ArchPartUi.Increment( Refresh ) );
}
void Types( ToolSidebarWidget panel, ArchWorkflowStep step ) {
var offered = Offered();
var removed = ArchStorage.HiddenArchetypes();
if ( offered.Count == 0 && removed.Count == 0 ) {
step.Layout.Add( ArchSidebarLayout.Advice( mode == BuildingMode.Prefab
? "No building type carries a canned layout. Use New with a type chosen instead."
: $"No building types. Authored ones live in Assets/{ArchStorage.ArchetypeDirectory}." ) );
return;
}
var shelf = new ArchPresetBrowser<ArchArchetype>( panel, Owner.Kit, Scope( "types" ), "Building types" ) {
Chosen = type => string.Equals( archetype, type.Name, StringComparison.OrdinalIgnoreCase ),
Choose = type => Adopt( type, step ),
Design = () => new ArchBuildingDesigner( panel, Owner, Chosen, designed => ApplyDesigned( designed, step ) ).Show(),
Reload = Reread,
Remove = item => Remove( item.Value ),
Restore = () => {
ArchStorage.RestoreArchetypes();
Reread();
}
};
shelf.DesignButton.Visible = true;
shelf.DesignButton.ToolTip = "Design a building type";
shelf.ReloadButton.Visible = true;
shelf.RestoreButton.Visible = removed.Count > 0;
shelf.RestoreButton.ToolTip = $"Bring back {string.Join( ", ", removed )}";
shelf.Set( offered );
step.Add( shelf );
}
void Stamps( ToolSidebarWidget panel, ArchWorkflowStep step ) {
if ( savedOffered.Count == 0 ) {
step.Layout.Add( ArchSidebarLayout.Advice( "No saved buildings yet. Right-click a building in the Plan Layers stack and Save, and it turns up here." ) );
return;
}
var shelf = new ArchPresetBrowser<ArchBuildingAsset>( panel, Owner.Kit, Scope( "stamps" ), "Saved buildings" ) {
Chosen = asset => saved is not null && string.Equals( saved.Name, asset.Name, StringComparison.OrdinalIgnoreCase ),
Choose = asset => {
Hold( asset );
step.Chose();
},
Reload = RereadSaved,
Remove = item => Discard( item.Value )
};
shelf.ReloadButton.Visible = true;
shelf.Set( savedOffered );
step.Add( shelf );
}
List<ArchPresetItem<ArchBuildingAsset>> Kept() {
return ArchLayerAssets.Buildings()
.Select( asset => new ArchPresetItem<ArchBuildingAsset> {
Value = asset,
Name = asset.Title,
Detail = ArchBuildingStamp.Describe( asset ),
Identity = ArchBuildingStamp.Identity( asset ),
Category = "Saved",
Glyph = "home_work",
Tags = asset.Name,
View = ArchPresetPreview.Quarter,
Focus = ArchPreviewFocus.Whole,
Recipe = () => ArchBuildingStamp.Stage( asset )
} )
.ToList();
}
void Discard( ArchBuildingAsset asset ) {
if ( !ArchLayerAssets.RemoveBuilding( asset.Name ) ) {
Log.Warning( $"Architecture: could not remove the saved building '{asset.Title}'." );
return;
}
if ( saved is not null && string.Equals( saved.Name, asset.Name, StringComparison.OrdinalIgnoreCase ) ) {
Hold( null );
}
RereadSaved();
Refresh();
}
void Hold( ArchBuildingAsset asset ) {
saved = asset;
savedGhost = asset is null ? null : ArchBuildingStamp.Ghost( asset );
}
void RereadSaved() {
var chosen = saved?.Name;
savedStamp = ArchLayerAssets.Revision;
savedOffered = Kept();
Hold( savedOffered.Select( item => item.Value ).FirstOrDefault( asset => string.Equals( asset.Name, chosen, StringComparison.OrdinalIgnoreCase ) ) );
}
void Reread() {
Owner.ReloadArchetypes();
Seed();
Refresh();
}
void Remove( ArchArchetype type ) {
if ( !ArchStorage.RemoveArchetype( type.Name ) ) {
Log.Warning( $"Architecture: could not remove the building type '{type.Name}'." );
return;
}
Owner.ReloadArchetypes();
// A drag reads its heights off the chosen type, so removing that one has to leave another standing.
if ( Owner.FindArchetype( archetype ) is null ) {
archetype = Owner.Archetypes.FirstOrDefault()?.Name ?? "";
}
Seed();
Refresh();
}
List<ArchPresetItem<ArchArchetype>> Offered() {
return Owner.Archetypes
.Where( type => mode != BuildingMode.Prefab || type.HasLayout )
.Select( type => new ArchPresetItem<ArchArchetype> {
Value = type,
Name = type.Title,
Detail = ArchArchetypeStage.Describe( type ),
Identity = ArchArchetypeStage.Identity( type ),
Category = type.HasLayout ? "Has layout" : "Shell",
Badge = type.HasLayout ? "layout" : null,
Glyph = type.Icon,
Tags = $"{type.Name} {type.Description}",
View = ArchPresetPreview.Quarter,
Focus = ArchPreviewFocus.Whole,
Recipe = () => ArchArchetypeStage.Of( type, Owner.Kit )
} )
.ToList();
}
void Inherit() {
if ( !Extending || seeded ) {
return;
}
if ( Owner.ActiveBuilding()?.Archetype is { Length: > 0 } existing && Owner.FindArchetype( existing ) is not null ) {
archetype = existing;
}
Seed();
}
void Adopt( ArchArchetype type, ArchWorkflowStep step ) {
archetype = type.Name;
Seed();
Restyle();
step.Chose();
}
void ApplyDesigned( ArchArchetype designed, ArchWorkflowStep step ) {
archetype = designed.Name;
Owner.RegisterArchetype( designed );
Seed();
Restyle();
step.Chose();
}
void Seed() {
if ( Chosen is not { } type ) {
return;
}
var rules = Extending ? type.Wing : type.Shell;
if ( rules.Roof is { } style ) {
roofStyle = style;
}
wingRoof = type.WingRoof;
eaveDrop = type.WingEaveDrop;
wallHeight = rules.WallHeight;
wallThickness = rules.WallThickness > 0f ? rules.WallThickness : rules.Wall?.Thickness ?? 0f;
roofThickness = rules.RoofThickness;
roofPitch = rules.RoofPitch;
withGutters = rules.Gutters ?? true;
withFloor = rules.Floor ?? true;
withFoundation = rules.Foundation ?? true;
withFascia = rules.Fascia ?? true;
withSoffit = rules.Soffit ?? true;
withCeiling = rules.Ceiling ?? false;
withFrame = rules.Frame ?? false;
seeded = true;
}
void Ridge( Layout roof ) {
if ( !Wants( ArchOptionGroup.Ridge ) || !Roofed
|| !ArchRoofPlane.Ridged( Extending ? ArchBuild.Winged( wingRoof ) : roofStyle ) ) {
return;
}
using var grid = ArchIconGrid.In( roof );
foreach ( var value in Enum.GetValues<RidgeRun>() ) {
var captured = value;
grid.Pick( ArchIcons.RidgeAdvice( captured ), ArchIcons.RidgeSlug( captured ), ArchIcons.RidgeGlyph( captured ), ridge == captured,
() => Set( () => ridge = captured ) );
}
}
static string Fallback( SectionRoof choice ) => choice switch {
SectionRoof.Continue => "merge_type",
SectionRoof.Hip => "roofing",
SectionRoof.Gable => "change_history",
SectionRoof.Capped => "crop_din",
_ => "block"
};
static string Describe( SectionRoof choice ) => choice switch {
SectionRoof.Continue => "Continue the roof (one hip, valleys)",
SectionRoof.Hip => "Own hip, stepped down",
SectionRoof.Gable => "Own gable, stepped down",
SectionRoof.Capped => "Flat with a parapet cap",
_ => "No roof"
};
}