Editor subtool for creating and editing walkway architectural links. It renders previews, handles drag placement, builds UI options for width/height/rise/interior/cornice/skirting/piers, and applies changes to the plan via ArchWalkway and related types.
using System;
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 section and footing tune 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;
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 Adopt( ArchPillarType type )
{
draft = type.Standing( default, 0f, 0f );
Refresh();
}
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();
}
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." ) );
}
ArchSidebarSection.Show( panel, Scope( "placement" ), "Placement", span =>
{
span.Add( ArchPartUi.Number( "Width", link is null ? width : ArchWalkway.WidthOf( link ), 72f,
value => { if ( link is null ) { width = value; } else { Rewidth( link, value ); } } ) );
// A rise decides which STOREY the far end opens at, and that is resolved by the placement rather than
// held on the link - so it seeds the next drag and an existing link changes storeys by being redrawn.
if ( link is null )
{
span.Add( ArchPartUi.Integer( "Rise (storeys)", rise, 0, value => rise = Math.Clamp( value, 0, 3 ) ) );
}
span.Add( ArchPartUi.Number( "Height (floor to ceiling)", height, 96f,
value => { if ( link is null ) { height = value; } else { Reheight( link, value ); } } ) );
} );
if ( link is null ? rise > 0 : Rising( link ) )
{
Interior( panel, link );
}
Finish( panel, link );
Piers( panel, link );
}
// Stairs, a ramp, or a plain slope carry the climb inside a rising link.
void Interior( ToolSidebarWidget panel, ArchRoom link )
{
var current = link?.Interior ?? interior;
ArchSidebarSection.Show( panel, Scope( "interior" ), "Inside the climb", group =>
{
using var grid = ArchIconGrid.In( group );
grid.Pick( "A plain slope to walk up", "opt_slope", "landscape", current == WalkwayInterior.None,
() => ChooseInterior( link, WalkwayInterior.None ) );
grid.Pick( "A flight of stairs", "opt_stairs", "stairs", current == WalkwayInterior.Stairs,
() => ChooseInterior( link, WalkwayInterior.Stairs ) );
grid.Pick( "A ramp with kerbs", "opt_ramp", "trending_up", current == WalkwayInterior.Ramp,
() => ChooseInterior( link, WalkwayInterior.Ramp ) );
} );
Underside( panel, link );
}
// A link stands clear of the ground, so what is under its flight is a face the street sees.
void Underside( ToolSidebarWidget panel, ArchRoom link )
{
if ( link?.Stairs.FirstOrDefault( stair => stair.Name == "WalkwayFlight" ) is not { } flight )
{
return;
}
ArchSidebarSection.Show( panel, Scope( "underside" ), "Under the climb", group =>
{
using var grid = ArchIconGrid.In( group );
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, () => { flight.Under = captured; Owner.Touch( "Walkway Underside" ); } );
}
} );
}
void ChooseInterior( ArchRoom link, WalkwayInterior value )
{
if ( link is null )
{
interior = value;
Refresh();
return;
}
ArchWalkway.SetInterior( Owner.Plan, link, Owner.Kit, value );
Owner.Touch( "Walkway Interior" );
}
// Cornice and skirting are dress on the placed link; without one they seed the next drag.
void Finish( ToolSidebarWidget panel, ArchRoom link )
{
var deck = link is null ? null : Deck( link );
ArchSidebarSection.Show( panel, Scope( "finish" ), "Finish", group =>
{
using var grid = ArchIconGrid.In( group );
grid.Toggle( "Cornice under the deck, along the walls", "opt_cornice", "border_top",
link is null ? cornice : deck?.Cornice ?? true,
value =>
{
if ( link is null )
{
cornice = value;
}
else if ( deck is not null )
{
deck.Cornice = value;
Owner.Touch( "Walkway Cornice" );
}
Refresh();
} );
grid.Toggle( "Skirting boards along the walls", "opt_skirting", "format_align_center",
link is null ? skirting : link.WalkwaySkirting,
value =>
{
if ( link is null )
{
skirting = value;
}
else
{
link.WalkwaySkirting = value;
Owner.Touch( "Walkway Skirting" );
}
Refresh();
} );
} );
}
// Always shown - hiding them until a link has some hides them when wanted. On an adopted link the toggle
// STANDS them or takes them away, because a link is the thing being edited and its piers are part of it.
void Piers( ToolSidebarWidget panel, ArchRoom link )
{
var standing = link?.Pillars.FirstOrDefault();
// Nothing to stand piers in, so say that rather than offer a toggle the support service will refuse.
if ( link is not null && !ArchWalkway.Raised( link, Owner.Kit ) )
{
ArchSidebarSection.Show( panel, Scope( "piers" ), "Piers underneath", group =>
group.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;
}
ArchSidebarSection.Show( panel, Scope( "piers" ), "Piers underneath", group =>
{
using var grid = ArchIconGrid.In( group );
grid.Toggle( "Stand piers under the link where it is raised above grade", "opt_pillars", "view_column",
link is null ? piers : standing is not null,
value =>
{
if ( link is null )
{
piers = value;
}
else
{
ArchWalkway.SetPiers( Owner.Plan, link, Owner.Kit, value ? draft : null );
Owner.Touch( "Walkway Piers" );
}
Refresh();
} );
} );
// A pier IS a pillar type, so its section, footing and span are the pillar designer's and are folded
// away here. A link's piers are tuned ON the link; with none standing there is nothing to tune, and
// the SEED editor shown there instead wrote every click into the next drag while the author watched
// the selection not move.
if ( link is null )
{
if ( piers )
{
ArchSidebarSection.Disclosure( panel, Scope( "pier" ), "Pier type", true, group =>
ArchSidebarLayout.Form( group, form =>
{
ArchAsks.PillarKinds( form, Owner.OfferedPillars(), draft.Type, Adopt );
ArchAsks.BuildPillar( form, draft, Owner.FindPillarType( draft.Type ), Refresh, null );
} ) );
}
return;
}
if ( standing is null )
{
return;
}
ArchSidebarSection.Disclosure( panel, Scope( "pier" ), "Pier type", true, group =>
ArchSidebarLayout.Form( group, form =>
{
ArchAsks.PillarKinds( form, Owner.OfferedPillars(), standing.Type,
type => { ArchAsks.RetypePillar( Owner, standing, type ); Refresh(); } );
ArchAsks.SizePillar( form, standing, Owner.FindPillarType( standing.Type ), Refresh, () => Owner.Touch( "Retune Piers" ) );
} ) );
}
}