Editor UI sheet for a stair step (ArchStairLane). Builds a sidebar panel with controls to edit length, width, bearing, rise, turn, and buttons to add/segment flights, add platforms, add railings, and delete the step. Includes helper methods to settle layout and describe turn icons.
using System;
using Editor;
namespace Sunless.Architecture;
public sealed class ArchStairStepSheet : IArchSheet
{
public Type Payload => typeof( ArchStairLane );
public void Build( ToolSidebarWidget panel, ArchTool tool, ArchSelection picked, Action refresh, Action changed )
{
if ( picked.Item is not ArchStairLane lane || ArchStairSteps.Owner( tool.Plan, lane ) is not { } stair )
{
return;
}
var ordinal = ArchStairSteps.Ordinal( stair, lane );
var group = panel.AddGroup( lane.Climbs ? $"Flight {ordinal}" : $"Platform {ordinal}" );
group.Add( ArchPartUi.Number( lane.Climbs ? "Run" : "Depth", lane.Length, 0f,
value => Settled( stair, lane, () => ArchStairEdges.Lengthen( lane, value ) ), changed ) );
group.Add( ArchPartUi.Number( "Width", lane.Width, stair.Width,
value => Settled( stair, lane, () => ArchStairEdges.Widen( lane, value ) ), changed ) );
if ( lane.Climbs )
{
// One segment of a curve, aimed by hand. The whole sweep is cut in the stair drawing; this is where a
// segment that came out a few degrees short of the wall it runs along is put right.
group.Add( ArchPartUi.Number( "Bearing", lane.Bearing, 0f,
value => ArchStairSteps.Bend( stair, lane, value ), changed ) );
}
// Zero is the whole point: an unpinned flight shares what the pinned ones left, and an unpinned platform
// is level. Typing a number here is the same act as pulling the step's lift arrow.
group.Add( ArchPartUi.Number( lane.Climbs ? "Climbs (0 divides)" : "Lift (0 is level)", lane.Rise, 0f,
value => lane.Rise = MathF.Max( 0f, value ), changed ) );
// The same turn the drawing offers, on the step picked in the viewport - a corner is authored wherever the
// step is held, not only in the window.
if ( ArchStairSteps.IndexOf( stair, lane ) > 0 )
{
var turning = panel.AddGroup( "Turn off the step below" );
using var grid = ArchIconGrid.In( turning );
foreach ( var value in Enum.GetValues<StairTurn>() )
{
var captured = value;
grid.Pick( Describe( captured ), $"turn_{captured}".ToLowerInvariant(), Glyph( captured ),
Aimed( stair, lane ) == captured, () =>
{
if ( ArchStairSteps.Aim( stair, lane, captured ) )
{
tool.Commit( "Turn Step" );
refresh?.Invoke();
}
} );
}
}
var flow = panel.AddGroup( "Step" ).AddRow();
flow.Spacing = 4;
flow.Add( new Button( "Flight after", "stairs" )
{
Clicked = () =>
{
if ( ArchStairSteps.Add( tool.Plan, stair, lane ) is not null )
{
tool.Commit( "Add Flight" );
refresh?.Invoke();
}
}
} );
if ( lane.Climbs )
{
flow.Add( new Button( "Segment", "content_cut" )
{
Clicked = () =>
{
if ( ArchStairSteps.Segment( tool.Plan, stair, lane, 2 ) )
{
tool.Commit( "Segment Flight" );
refresh?.Invoke();
}
}
} );
}
if ( lane.Rise > 0.05f )
{
flow.Add( new Button( lane.Climbs ? "Let it divide" : "Lie level", "restart_alt" )
{
Clicked = () => { lane.Rise = 0f; changed?.Invoke(); }
} );
}
if ( lane.Climbs )
{
flow.Add( new Button( "Platform after", "horizontal_rule" )
{
Clicked = () =>
{
if ( ArchStairSteps.AddLanding( tool.Plan, stair, lane ) is not null )
{
tool.Commit( "Add Platform" );
refresh?.Invoke();
}
}
} );
}
flow.Add( new Button( "Railing", "fence" )
{
Clicked = () =>
{
lane.Guards.Add( new ArchStairGuardPart
{
Id = tool.Plan.AllocateId(),
// The open side, so the first railing added lands where a drop actually is rather than
// against whatever the flight runs along.
Edge = lane.Guarding( StairEdge.Left ) ? StairEdge.Right : StairEdge.Left
} );
tool.Commit( "Add Railing" );
refresh?.Invoke();
}
} );
flow.Add( new Button( "Delete step", "backspace" )
{
Clicked = () =>
{
if ( ArchStairSteps.Remove( stair, lane ) )
{
tool.Select( null );
tool.Commit( lane.Climbs ? "Delete Flight" : "Delete Platform" );
refresh?.Invoke();
}
}
} );
}
// A number typed on the sheet moves the steps around it exactly as a dragged edge does - there is one answer to
// what a chain does when one of its steps changes size, and it is the settle pass.
static void Settled( ArchStairPart stair, ArchStairLane lane, Action edit )
{
edit();
ArchStairSteps.Settle( stair, lane );
ArchStairLanes.Fit( stair.Core, stair.Lanes );
}
static StairTurn Aimed( ArchStairPart stair, ArchStairLane lane )
{
var index = ArchStairSteps.IndexOf( stair, lane );
return stair.Lanes[index - 1].Walk.Between( lane.Walk );
}
static string Describe( StairTurn turn ) => turn switch
{
StairTurn.Left => "Turn left at the head of the step below",
StairTurn.Right => "Turn right at the head of the step below",
StairTurn.Back => "Double back beside the step below",
_ => "Carry straight on"
};
static string Glyph( StairTurn turn ) => turn switch
{
StairTurn.Left => "turn_left",
StairTurn.Right => "turn_right",
StairTurn.Back => "u_turn_left",
_ => "straight"
};
}