Editor subtool for building placement in the architecture editor. Handles modes (New, Extend, Prefab, Row), draws placement previews, resolves placements, commits building/wing/row placements, and provides sidebar UI for options and archetype selection.
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
public enum BuildingMode
{
New,
Extend,
Prefab,
Row
}
[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;
// Off: a dropped eave opens a void under the storey grid above it.
float eaveDrop;
// Seeded from the type then editable - an invisible number can't be matched by hand.
float wallHeight;
float roofPitch;
bool withFrame;
bool withFascia = true;
bool withSoffit = true;
bool withCeiling;
bool seeded;
ArchPresetBrowser<ArchArchetype> browser;
// Re-dresses while still the newest in the plan; anything else authored moves the counter.
int placedRoom;
int placedRoof;
int placedStamp;
bool placedMerged;
bool Extending => mode == BuildingMode.Extend;
bool Rowing => mode == BuildingMode.Row;
ArchArchetype Chosen => Owner.FindArchetype( archetype );
protected override string Title() => "Building";
// Whether the section this drag stands will carry a roof at all - a wing answers through its own rules.
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.",
_ => "Drag the outer shell. The chosen type decides its heights, roof and trims."
};
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;
}
ArchGhost.Volume( placement.Min, placement.Max, Owner.LevelHeight, Owner.LevelHeight + height );
ArchGhost.Note( new Vector3( placement.Max.x, placement.Max.y, Owner.LevelHeight + height ),
$"{placement.Max.x - placement.Min.x:0} x {placement.Max.y - placement.Min.y:0}" );
if ( !withRoof && !Extending )
{
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 ) );
}
// From the layout's resolved rectangles, so growth and placement show before release.
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}" );
}
// From the one resolve the commit uses, so the units drawn are the units stood.
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();
// The drag names the building it joins, not the target picker - whose placeholder extends nothing.
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.Commit();
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 );
Owner.ActiveBuildingId = building.Id;
Owner.ActiveRoomId = shell.Room.Id;
FinishDraft( building.Id );
Owner.Commit();
Remember( shell );
}
ArchRectanglePlacement Resolve( Vector2 from, Vector2 to )
{
return new ArchBoundaryPlacementService( 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();
}
// Shown in the panel - an untold live edit reads as the tool acting alone.
ArchRoom Editing => placedRoom != 0 && Owner.Plan.NextId == placedStamp ? Owner.Plan.FindRoom( placedRoom ) : null;
// Edits the last drag's room and roof in place - nothing is re-created.
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;
}
// A merged wing shares the section it folded into; style and ridge follow the host.
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();
}
// A choice that decides which rows exist re-lays out only after the standing section has been re-dressed;
// refreshing from inside the change tears down the widget still handling the click.
void Relayout( Action change )
{
Set( change );
Refresh();
}
// The type only seeds these - every deciding number is visible and editable.
ArchSectionRules Authored()
{
var type = Chosen is { } chosen ? (Extending ? chosen.Wing : chosen.Shell) : new ArchSectionRules();
return new ArchSectionRules
{
WallHeight = wallHeight,
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.Commit( $"Place {recipe.Title}" );
}
ArchPlot ResolvedPrefabPlot( ArchArchetype recipe, Vector2 from, Vector2 to, out bool usable )
{
return new ArchBoundaryPlacementService( 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.Commit( $"Place Street Row of {placed.Buildings.Count}" );
}
// The road comes from the tool's own memoised lookup - Nearest walks a whole dense curve, and the ghost asks per frame.
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 );
}
protected override void BuildOptions( ToolSidebarWidget panel )
{
ArchSidebarSection.Show( panel, Scope( "mode" ), "Mode", group =>
{
using var grid = ArchIconGrid.In( group );
Mode( grid, BuildingMode.New, "mode_new_building", "New building — drag the outer shell", "domain_add" );
Mode( grid, BuildingMode.Extend, "mode_extend_building", "Extend the active building — drag a wing onto it", "add_home_work" );
Mode( grid, BuildingMode.Prefab, "mode_archetype", "Place the type's canned layout — one drag lays the whole unit out", "auto_awesome_motion" );
Mode( grid, 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" );
} );
Types( panel );
// A canned layout decides its own steps; the shell's knobs would contradict it.
if ( mode == BuildingMode.Prefab )
{
Orientation( panel );
return;
}
// Foundation and the include flags are one question - what this shell is made of - and were two boxes
// of icons asking it. Two grids inside one section, because a Pick is exclusive within its own grid.
ArchSidebarSection.Show( panel, Scope( "include" ), "Include", group =>
{
if ( Wants( ArchOptionGroup.Foundation ) )
{
using var footing = ArchIconGrid.In( group );
footing.Pick( "Raised foundation — the plinth stands proud of grade and the building sits up on it",
"foundation_raised", "vertical_align_top", withFoundation, () => Set( () => withFoundation = true ) );
footing.Pick( "Nested foundation — the footing is buried and the floor sits on grade",
"foundation_nested", "vertical_align_bottom", !withFoundation, () => Set( () => withFoundation = false ) );
}
using var grid = ArchIconGrid.In( group );
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 ) );
}
} );
if ( Wants( ArchOptionGroup.Section ) )
{
Section( panel );
}
if ( !Extending )
{
// Nothing under here describes a section with no roof over it.
if ( !withRoof )
{
return;
}
ArchSidebarSection.Show( panel, Scope( "roof" ), "Roof", group =>
{
if ( Wants( ArchOptionGroup.RoofStyle ) )
{
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 ), roofStyle == captured,
() => Relayout( () => roofStyle = captured ) );
}
}
Ridge( group );
} );
return;
}
if ( !Wants( ArchOptionGroup.WingRoof ) )
{
return;
}
ArchSidebarSection.Show( panel, Scope( "wing" ), "Wing roof", wings =>
{
using ( var grid = ArchIconGrid.In( wings ) )
{
foreach ( var value in Enum.GetValues<SectionRoof>() )
{
var captured = value;
grid.Pick( Describe( captured ), $"wing_{captured}".ToLowerInvariant(), Fallback( captured ), wingRoof == captured,
() => Relayout( () => wingRoof = captured ) );
}
}
// Continue takes the host's eave, and no roof has no eave to drop.
if ( wingRoof is not (SectionRoof.Continue or SectionRoof.None) )
{
wings.Add( ArchPartUi.Number( "Eave drop", eaveDrop, 0f, value => Set( () => eaveDrop = value ) ) );
}
Ridge( wings );
} );
}
bool Wants( ArchOptionGroup group ) => Chosen?.Wants( group ) ?? true;
void Mode( ArchIconGrid grid, BuildingMode choice, string slug, string tooltip, string fallback )
{
grid.Pick( tooltip, slug, fallback, mode == choice, () =>
{
mode = choice;
// Cleared, not reseeded - Extend picks up the active building's own type first.
seeded = false;
Refresh();
} );
}
// The type's own numbers, editable - a wing must match the shell it joins. The two that decide where the
// drag lands stay open; the lining you would judge on a section already standing folds away.
void Section( ToolSidebarWidget panel )
{
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." ) );
}
ArchSidebarSection.Show( panel, Scope( "placement" ), "Placement", group =>
{
group.Add( ArchPartUi.Number( "Wall height", wallHeight, 0f, value => Set( () => wallHeight = value ) ) );
// Pitch describes a deck; with no roof over the section there is none.
if ( Roofed )
{
group.Add( ArchPartUi.Number( "Roof pitch", roofPitch, 0f, value => Set( () => roofPitch = value ) ) );
}
} );
ArchSidebarSection.Disclosure( panel, Scope( "construction" ), "Construction", true, group =>
{
using var grid = ArchIconGrid.In( group );
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 ) );
} );
}
// Picking re-types it - being unable to change your mind is worse than a mismatch. The designer rides the
// browser's own header, where every other authored kit lives; a full-width button of its own put the way
// into the designer above the thing it designs.
void Types( ToolSidebarWidget panel )
{
Inherit();
var offered = Offered();
var removed = ArchStorage.HiddenArchetypes();
if ( offered.Count == 0 && removed.Count == 0 )
{
panel.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;
}
browser = new ArchPresetBrowser<ArchArchetype>( panel, Owner.Kit, Scope( "types" ), "Building types" )
{
Chosen = type => string.Equals( archetype, type.Name, StringComparison.OrdinalIgnoreCase ),
Choose = Adopt,
Design = () => new ArchBuildingDesigner( panel, Owner, Chosen, ApplyDesigned ).Show(),
Reload = Reread,
Remove = item => Remove( item.Value ),
Restore = () =>
{
ArchStorage.RestoreArchetypes();
Reread();
}
};
browser.DesignButton.Visible = true;
browser.DesignButton.ToolTip = "Design a building type";
browser.ReloadButton.Visible = true;
browser.RestoreButton.Visible = removed.Count > 0;
browser.RestoreButton.ToolTip = $"Bring back {string.Join( ", ", removed )}";
browser.Set( offered );
panel.Layout.Add( browser );
}
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();
}
// A canned layout is the whole gesture in Prefab mode, so a type without one is not a choice there.
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();
}
// Shown once - re-reading it every rebuild would undo the re-type pick.
void Inherit()
{
if ( !Extending || seeded )
{
return;
}
if ( Owner.ActiveBuilding()?.Archetype is { Length: > 0 } existing && Owner.FindArchetype( existing ) is not null )
{
archetype = existing;
}
Seed();
}
// Seeds every switch the type has an opinion about, so the sidebar matches the drag.
void Adopt( ArchArchetype type )
{
archetype = type.Name;
Seed();
Restyle();
Refresh();
}
// Adopts like a grid pick, then re-dresses the standing section.
void ApplyDesigned( ArchArchetype designed )
{
archetype = designed.Name;
Owner.RegisterArchetype( designed );
Seed();
Restyle();
Refresh();
}
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;
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 Orientation( ToolSidebarWidget panel )
{
ArchSidebarSection.Show( panel, Scope( "orientation" ), "Orientation", group =>
{
using var grid = ArchIconGrid.In( group );
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 );
} );
}
// Its own grid inside whichever roof section called it - a ridge direction and a roof style are two
// exclusive choices, and sharing one grid would unlight the style when a direction was picked.
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"
};
}