Editor/Tool/Subtools/ArchWalkwaySubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
[Title( "Walkway" ), Icon( "linear_scale" ), Group( "07" )]
public sealed class ArchWalkwaySubtool( ArchTool owner ) : ArchSubtool( owner ) {
protected override ArchKind? DraftKind => ArchKind.Walkway;
public override ArchSurface[] Surfaces => new[]
{
ArchSurface.WallExterior, ArchSurface.WallInterior, ArchSurface.Floor, ArchSurface.Roof,
ArchSurface.Ceiling, ArchSurface.Baseboard, ArchSurface.Trim, ArchSurface.Pillar, ArchSurface.PillarCap
};
float width = 72f;
float height = 96f;
int rise;
bool piers = true;
WalkwayInterior interior;
bool cornice = true;
bool skirting = true;
// Held here so the pier type is answered BEFORE the drag, not only after.
ArchPillarPart draft = new();
// Re-dresses the newest link in the plan; authoring anything else moves the counter.
int placed;
int stamp;
protected override string Title() => "Walkway";
protected override string Advice() => "Drag the link between two shells. Both ends open into whatever wall they land on.";
ArchRoom Placed => placed != 0 && (adopted || Owner.Plan.NextId == stamp) ? Owner.Plan.FindRoom( placed ) : null;
// A link taken from the stack is edited live like one just dragged - but it stays editable
// through commits, where the freshly-placed one is only current until the plan moves on.
bool adopted;
public override bool Adopts => true;
public override void OnEnabled() {
base.OnEnabled();
ArchWorkflow.Restart( Scope( "flow" ) );
}
protected override void Adopt( ArchSelection picked ) {
adopted = picked?.Item is ArchRoom { Spans: true };
placed = adopted ? ((ArchRoom)picked.Item).Id : 0;
stamp = Owner.Plan.NextId;
if ( Placed is { } link ) {
height = MathF.Max( 60f, (Deck( link )?.BaseHeight ?? link.BaseHeight + link.WallHeight) - link.BaseHeight );
}
}
ArchRoofPart Deck( ArchRoom link ) {
return Owner.Plan.Buildings
.SelectMany( building => building.Roofs )
.FirstOrDefault( roof => roof.Level == link.Floor && MathF.Abs( roof.BaseHeight - (link.BaseHeight + height) ) < link.WallHeight );
}
bool Rising( ArchRoom link ) => link is not null && link.WalkwayTop > link.BaseHeight + 1f;
// The drag squared onto its long axis and widened to the Width box.
protected override void DrawPreview() {
ArchWalkway.Shape( Min( DragStart, DragCurrent ), Max( DragStart, DragCurrent ), width, out var min, out var max, out var alongX );
var deck = Owner.LevelHeight;
var head = deck + MathF.Max( 60f, height );
ArchGhost.Volume( min, max, deck, head );
ArchGhost.Plate( min, max, deck, 8 );
var run = alongX ? max.x - min.x : max.y - min.y;
var across = alongX ? max.y - min.y : max.x - min.x;
ArchGhost.Note( new Vector3( max.x, max.y, head ), $"walkway — {run:0} x {across:0}" );
}
protected override void OnDrag( Vector2 from, Vector2 to ) {
var link = ArchWalkway.Add(
Owner.Plan, Owner.ActiveBuilding(), Owner.Kit, Owner.Level, Owner.LevelHeight,
Min( from, to ), Max( from, to ), from, width, height, piers ? draft : null,
rise, interior, cornice, skirting );
if ( link is null ) {
Log.Info( "Architecture: that drag leaves nothing to span - it needs to reach across the gap between two shells." );
return;
}
Owner.ActiveBuildingId = link.Host.Id;
Owner.ActiveRoomId = link.Room.Id;
FinishDraft( link.Room.Id );
// The link is a site connection: it forms the group holding it and both houses it joined,
// so the tree shows the join and the mouths know which walls they are allowed to stand in.
Owner.InsertionTarget = null;
ArchWalkwayConnection.Form( Owner.Plan, link );
Owner.Commit( "Create Walkway" );
placed = link.Room.Id;
stamp = Owner.Plan.NextId;
Refresh();
if ( link.Breached == 0 ) {
Log.Info( "Architecture: the walkway reached no wall at either end, so it opens into nothing yet." );
}
}
void Seeded() {
if ( draft.Type.Length > 0 || Owner.DefaultPillar() is not { } first ) {
return;
}
draft = first.Standing( default, 0f, 0f );
}
void Reheight( ArchRoom link, float value ) {
height = value;
ArchWalkway.Reheight( link, Deck( link ), Owner.Kit, value );
Owner.Touch( "Walkway Height" );
}
// Refreshed, unlike the height: the reshape clamps a width the passage cannot take and re-seats both
// ends, so the box has to show back what the link actually settled on.
void Rewidth( ArchRoom link, float value ) {
width = value;
ArchWalkway.SetWidth( Owner.Plan, link, Owner.Kit, value );
Owner.Touch( "Walkway Width" );
Refresh();
}
// How far it climbs, what carries the climb, what the street sees under it, what stands beneath it, what it
// wears, then how big it is - every pick before the two typed sizes, which can never answer their own step.
//
// Each step is asked only where the link can have one: the rise seeds the next drag and a placed link changes
// storeys by being redrawn, the underside exists only once a flight has been cut, and a link sitting on grade
// has no gap to stand piers in and says so instead of offering a control the support service would refuse.
protected override void BuildOptions( ToolSidebarWidget panel ) {
Seeded();
var link = Placed;
if ( link is not null ) {
panel.Layout.Add( ArchSidebarLayout.Advice( $"Editing {link.Name} live. Drag anything else and these go back to seeding the next link." ) );
}
using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );
if ( link is null ) {
flow.Step( "Rise", RiseName, RiseGlyph, PickRise );
}
if ( link is null ? rise > 0 : Rising( link ) ) {
flow.Step( "Climb", ClimbName( link ), ClimbGlyph( link ), step => PickClimb( link, step ) );
if ( Flight( link ) is not null ) {
flow.Step( "Underside", UndersideName( link ), UndersideGlyph( link ), step => PickUnderside( link, step ) );
}
}
flow.Step( "Piers", PiersName( link ), PiersGlyph( link ), step => PickPiers( link, step ) );
flow.Step( "Finish", FinishName( link ), "format_paint", step => BuildFinish( link, step ) );
flow.Step( "Span", SpanName( link ), "straighten", step => BuildSpan( link, step ) );
}
static string Storeys( int count ) => count == 1 ? "1 storey" : $"{count} storeys";
string RiseName => rise == 0 ? "Level" : Storeys( rise );
string RiseGlyph => rise == 0 ? "horizontal_rule" : "trending_up";
// A rise is three legal answers and level, so it is picked rather than typed - and picked first, because
// everything below it is asked only of a link that climbs.
void PickRise( ArchWorkflowStep step ) {
using var grid = ArchIconGrid.In( step.Layout );
grid.Pick( "Level — both ends open at the same storey", "rise_level", "horizontal_rule",
rise == 0, () => Raise( 0, step ) );
for ( var storeys = 1; storeys <= 3; storeys++ ) {
var captured = storeys;
grid.Pick( $"{Storeys( captured )} — the far end opens that much higher", $"rise_{captured}", "trending_up",
rise == captured, () => Raise( captured, step ) );
}
}
void Raise( int storeys, ArchWorkflowStep step ) {
rise = storeys;
step.Chose();
}
WalkwayInterior Inside( ArchRoom link ) => link?.Interior ?? interior;
string ClimbName( ArchRoom link ) => Inside( link ) switch {
WalkwayInterior.Stairs => "Stairs",
WalkwayInterior.Ramp => "Ramp",
_ => "Slope"
};
string ClimbGlyph( ArchRoom link ) => Inside( link ) switch {
WalkwayInterior.Stairs => "stairs",
WalkwayInterior.Ramp => "trending_up",
_ => "landscape"
};
void PickClimb( ArchRoom link, ArchWorkflowStep step ) {
using var grid = ArchIconGrid.In( step.Layout );
grid.Pick( "Slope — a plain grade to walk up", "opt_slope", "landscape",
Inside( link ) == WalkwayInterior.None, () => ChooseInterior( link, WalkwayInterior.None, step ) );
grid.Pick( "Stairs — a flight cut inside the link", "opt_stairs", "stairs",
Inside( link ) == WalkwayInterior.Stairs, () => ChooseInterior( link, WalkwayInterior.Stairs, step ) );
grid.Pick( "Ramp — a ramp with kerbs", "opt_ramp", "trending_up",
Inside( link ) == WalkwayInterior.Ramp, () => ChooseInterior( link, WalkwayInterior.Ramp, step ) );
}
void ChooseInterior( ArchRoom link, WalkwayInterior value, ArchWorkflowStep step ) {
if ( link is null ) {
interior = value;
} else {
ArchWalkway.SetInterior( Owner.Plan, link, Owner.Kit, value );
Owner.Touch( "Walkway Interior" );
}
step.Chose();
}
// A link stands clear of the ground, so what is under its flight is a face the street sees.
ArchStairPart Flight( ArchRoom link ) => link?.Stairs.FirstOrDefault( stair => stair.Name == "WalkwayFlight" );
string UndersideName( ArchRoom link ) => Flight( link ) is { } flight ? ArchStairLabels.Describe( flight.Under ) : "";
string UndersideGlyph( ArchRoom link ) => Flight( link ) is { } flight ? ArchStairLabels.Fallback( flight.Under ) : "crop_free";
void PickUnderside( ArchRoom link, ArchWorkflowStep step ) {
if ( Flight( link ) is not { } flight ) {
return;
}
using var grid = ArchIconGrid.In( step.Layout );
foreach ( var value in new[] { StairUnder.Filled, StairUnder.Open } ) {
var captured = value;
grid.Pick( ArchStairLabels.Describe( captured ), $"under_{captured}".ToLowerInvariant(), ArchStairLabels.Fallback( captured ),
flight.Under == captured, () => Underneath( flight, captured, step ) );
}
}
void Underneath( ArchStairPart flight, StairUnder under, ArchWorkflowStep step ) {
flight.Under = under;
Owner.Touch( "Walkway Underside" );
step.Chose();
}
bool Grounded( ArchRoom link ) => link is not null && !ArchWalkway.Raised( link, Owner.Kit );
string Carrying( ArchRoom link ) => link is null ? piers ? draft.Type : null : link.Pillars.FirstOrDefault()?.Type;
string PiersName( ArchRoom link ) {
if ( Grounded( link ) ) {
return "On grade";
}
return Carrying( link ) is { Length: > 0 } name ? Owner.FindPillarType( name )?.Title ?? name : "None";
}
string PiersGlyph( ArchRoom link ) {
if ( Grounded( link ) ) {
return "horizontal_rule";
}
return Carrying( link ) is { Length: > 0 } ? "view_column" : "block";
}
// One question - what carries the link - where a toggle used to govern a folded type list. A pier IS a
// pillar, so its section, footing and bays are the Pillar tool's to author and the pier's own sheet in
// Select to retune; standing them here from the same support service the drag uses gives a link given piers
// after the fact the bays the drag would have given it.
void PickPiers( ArchRoom link, ArchWorkflowStep step ) {
if ( Grounded( link ) ) {
step.Layout.Add( Wrapped( "This link sits on grade, so there is no gap under it to stand piers in. Raise it and they can go in." ) );
return;
}
var offered = Owner.OfferedPillars();
if ( offered.Count == 0 ) {
step.Layout.Add( ArchSidebarLayout.Advice( "No pillar types are authored — add one to Assets/arch/pillars." ) );
return;
}
var carrying = Carrying( link );
using var grid = ArchIconGrid.In( step.Layout );
grid.Pick( "None — the link spans clear", "opt_no_piers", "block",
string.IsNullOrEmpty( carrying ), () => Stand( link, null, step ) );
foreach ( var type in offered ) {
var captured = type;
grid.Pick( $"{captured.Title} — {captured.Description}", ArchIcons.PillarSlug( captured.Name ), captured.Icon,
string.Equals( carrying, captured.Name, StringComparison.OrdinalIgnoreCase ), () => Stand( link, captured, step ) );
}
}
void Stand( ArchRoom link, ArchPillarType type, ArchWorkflowStep step ) {
var source = type?.Standing( default, 0f, 0f );
if ( link is null ) {
piers = source is not null;
if ( source is not null ) {
draft = source;
}
} else {
ArchWalkway.SetPiers( Owner.Plan, link, Owner.Kit, source );
Owner.Touch( "Walkway Piers" );
}
step.Chose();
}
bool Corniced( ArchRoom link ) => link is null ? cornice : Deck( link )?.CeilingCornice ?? true;
bool Skirted( ArchRoom link ) => link is null ? skirting : link.WalkwaySkirting;
string FinishName( ArchRoom link ) {
var worn = new List<string>();
if ( Corniced( link ) ) {
worn.Add( "Cornice" );
}
if ( Skirted( link ) ) {
worn.Add( "skirting" );
}
return worn.Count == 0 ? "Bare" : string.Join( ", ", worn );
}
// Cornice and skirting are dress on the placed link; without one they seed the next drag.
void BuildFinish( ArchRoom link, ArchWorkflowStep step ) {
var deck = link is null ? null : Deck( link );
using var grid = ArchIconGrid.In( step.Layout );
grid.Toggle( "Cornice under the deck, along the walls", "opt_cornice", "border_top", Corniced( link ),
value => {
if ( link is null ) {
cornice = value;
} else if ( deck is not null ) {
deck.CeilingCornice = value;
Owner.Touch( "Walkway Cornice" );
}
} );
grid.Toggle( "Skirting boards along the walls", "opt_skirting", "format_align_center", Skirted( link ),
value => {
if ( link is null ) {
skirting = value;
} else {
link.WalkwaySkirting = value;
Owner.Touch( "Walkway Skirting" );
}
} );
}
string SpanName( ArchRoom link ) {
var across = link is null ? width : ArchWalkway.WidthOf( link );
return $"{across:0} wide, {MathF.Max( 60f, height ):0} high";
}
void BuildSpan( ArchRoom link, ArchWorkflowStep step ) {
step.Layout.Add( ArchPartUi.Number( "Width", link is null ? width : ArchWalkway.WidthOf( link ), 72f,
value => { if ( link is null ) { width = value; } else { Rewidth( link, value ); } } ) );
step.Layout.Add( ArchPartUi.Number( "Height (floor to ceiling)", height, 96f,
value => { if ( link is null ) { height = value; } else { Reheight( link, value ); } } ) );
}
}