Editor-side handlers, pickers, previews and effects for stair-related architecture parts. Draws and edits carved and exterior stairs with gizmo handles, picks stair geometry under cursor, produces preview ghosts, measures extents and triggers stair affectors when parts close.
namespace Sunless.Architecture;
public sealed class ArchStairHandler : IArchHandler
{
public ArchKind Kind => ArchKind.Stair;
public bool Draw( ArchTool tool, ArchSelection picked )
{
return picked.Item is ArchStairPart stair
&& ArchStairHandles.Draw( tool, stair, picked.Room, picked.Building );
}
}
// One step of a climb, picked in the stack: its own head, flanks and lift and nothing else. The stair it belongs
// to is looked up, because a step is filed on that stair's Lanes list and nowhere else in the plan.
public sealed class ArchStairStepHandler : IArchHandler
{
public ArchKind Kind => ArchKind.StairStep;
public bool Draw( ArchTool tool, ArchSelection picked )
{
if ( picked.Item is not ArchStairLane lane || ArchStairSteps.Owner( tool.Plan, lane ) is not { } stair )
{
return false;
}
return ArchStairHandles.Draw( tool, stair, picked.Room, picked.Building, stair.Lanes.IndexOf( lane ) );
}
}
public sealed class ArchCarvedStairHandler : IArchHandler
{
public ArchKind Kind => ArchKind.CarvedStair;
public bool Draw( ArchTool tool, ArchSelection picked )
{
if ( picked.Item is not ArchStairPart stair
|| tool.Plan.PlatformCarrying( stair ) is not { } platform
|| stair.Core is not { } core )
{
return false;
}
var gesture = ArchGesture.On( ArchKind.CarvedStair, stair.Id );
var top = platform.TopHeight;
var half = core.Width * 0.5f;
var start = core.Flat( 0f, half );
var end = core.Flat( core.Length, half );
var changed = false;
if ( ArchShapeHandles.Placed( tool, gesture.At( "foot" ), new Vector3( start.x, start.y, top ), out var foot, Gizmo.Colors.Green ) )
{
Refit( core, foot, end );
changed = true;
}
if ( ArchShapeHandles.Placed( tool, gesture.At( "head" ), new Vector3( end.x, end.y, top ), out var head, Gizmo.Colors.Green ) )
{
Refit( core, start, head );
changed = true;
}
var middle = (start + end) * 0.5f;
var edge = middle + core.Axes.Across * half;
if ( ArchShapeHandles.Width( tool, gesture.At( "width" ), new Vector3( edge.x, edge.y, top ), middle, core.Axes.Across, 12f, out var widened, Gizmo.Colors.Blue ) )
{
Widen( core, widened );
stair.Width = widened;
changed = true;
}
if ( ArchShapeHandles.Lift( tool, gesture.At( "rise" ), middle, stair.HeadHeight, out var risen, Gizmo.Colors.Roll ) )
{
core.Rise = MathF.Max( 4f, risen - stair.BaseHeight );
changed = true;
}
if ( changed )
{
Restretch( stair, core );
}
return changed;
}
// A carve is one straight notch, so its shaft is refitted from the two ends of its centreline.
static void Refit( ArchStairCore core, Vector2 foot, Vector2 head )
{
var span = head - foot;
if ( span.Length < 1f )
{
return;
}
core.Yaw = MathF.Atan2( span.y, span.x ).RadianToDegree();
core.Length = span.Length;
core.Origin = foot - core.Axes.Across * (core.Width * 0.5f);
}
static void Widen( ArchStairCore core, float width )
{
var centre = core.Flat( 0f, core.Width * 0.5f );
core.Width = MathF.Max( 1f, width );
core.Origin = centre - core.Axes.Across * (core.Width * 0.5f);
}
static void Restretch( ArchStairPart stair, ArchStairCore core )
{
foreach ( var lane in stair.Lanes )
{
lane.AlongFrom = 0f;
lane.AlongTo = core.Length;
lane.AcrossFrom = 0f;
lane.AcrossTo = core.Width;
}
}
}
public sealed class ArchExteriorStairHandler : IArchHandler
{
public ArchKind Kind => ArchKind.ExteriorStair;
public bool Draw( ArchTool tool, ArchSelection picked )
{
if ( picked.Item is not ArchExteriorStairPart flight )
{
return false;
}
var gesture = ArchGesture.On( ArchKind.ExteriorStair, flight.Id );
var changed = false;
if ( ArchShapeHandles.Move( tool, gesture, flight.Anchor, out var moved ) )
{
flight.Anchor = moved;
changed = true;
}
if ( ArchShapeHandles.Height( tool, gesture.At( "head" ), flight.Anchor, flight.TopHeight, out var head, Gizmo.Colors.Roll ) )
{
flight.TopHeight = MathF.Max( flight.BaseHeight + 12f, head );
changed = true;
}
if ( ArchShapeHandles.Height( tool, gesture.At( "foot" ), flight.Anchor, flight.BaseHeight, out var foot, Gizmo.Colors.Pitch ) )
{
flight.BaseHeight = MathF.Min( flight.TopHeight - 12f, foot );
changed = true;
}
return changed;
}
}
public sealed class ArchStairPicker : IArchPicker
{
public ArchKind Kind => ArchKind.Stair;
public void Under( ArchPicking picking )
{
if ( picking.Node.Payload is not ArchStairPart stair )
{
return;
}
if ( picking.Node.Room is { } room && room.Stairs.Contains( stair ) )
{
if ( ArchStairShape.Resolve( stair, room, picking.Kit ).Wells().Any( loop => ArchFootprint.Contains( loop, picking.Point ) ) )
{
picking.Hit( ArchHitChannel.Volume, 0f, true );
}
return;
}
ArchCarvedStairPicker.Pick( picking, stair );
}
}
public sealed class ArchCarvedStairPicker : IArchPicker
{
public ArchKind Kind => ArchKind.CarvedStair;
public void Under( ArchPicking picking )
{
if ( picking.Node.Payload is ArchStairPart stair )
{
Pick( picking, stair );
}
}
internal static void Pick( ArchPicking picking, ArchStairPart stair )
{
if ( stair.Core is { } core && ArchFootprint.Contains( core.Outline(), picking.Point ) )
{
picking.Hit( ArchHitChannel.Volume, 0f, true );
}
}
}
public sealed class ArchCarvedStairPreview : IArchPreview
{
public ArchKind Kind => ArchKind.CarvedStair;
public void Draw( ArchPreviewContext context )
{
if ( context.Payload is not ArchStairPart stair
|| context.Tool?.Plan.PlatformCarrying( stair ) is not { } platform
|| stair.Core is not { } core )
{
return;
}
var head = core.Flat( core.Length, core.Width * 0.5f );
ArchGhost.Prism( core.Outline(), platform.GradeHeight, platform.TopHeight );
ArchGhost.Note( new Vector3( head.x, head.y, platform.TopHeight ),
$"carve — {core.Length:0} run, {core.Width:0} wide, {stair.TotalRise:0} rise" );
}
}
public sealed class ArchStairPreview : IArchPreview
{
public ArchKind Kind => ArchKind.Stair;
public void Draw( ArchPreviewContext context )
{
if ( !context.Selected || context.Payload is not ArchStairPart stair || context.Room is null || context.Tool is null )
{
return;
}
foreach ( var well in ArchStairShape.Resolve( stair, context.Room, context.Tool.Kit, context.Building?.Rooms, context.Building ).Levels )
{
Gizmo.Draw.Color = Color.Yellow.WithAlpha( 0.5f );
foreach ( var outline in well.Loops )
{
ArchGhost.Ring( outline, well.Height );
}
}
}
}
public sealed class ArchStairExtentProvider : IArchExtentProvider
{
public ArchKind Kind => ArchKind.Stair;
public ArchExtent Measure( object payload, ArchExtentContext context )
{
var extent = new ArchExtent();
if ( payload is not ArchStairPart stair || context.Room is null )
{
return extent;
}
var shape = ArchStairShape.Resolve( stair, context.Room, context.Kit );
var top = shape.TopHeight + MathF.Max( 12f, context.Kit.HandrailHeight ) + context.Kit.NewelRise;
foreach ( var well in shape.Wells() )
{
extent.Add( well, shape.BaseHeight - context.Kit.StringerDepth - 4f, top );
}
return extent;
}
}
public sealed class ArchStairEffects : IArchAffector
{
public ArchKind Kind => ArchKind.Stair;
public int Order => 20;
public int Resolve( ArchPlan plan, ArchKit kit ) => ArchStairAffector.Resolve( plan, kit );
public bool Closes( object payload ) => payload is ArchStairPart;
public void Close( ArchPlan plan, object payload )
{
if ( payload is ArchStairPart stair )
{
ArchStairAffector.Remove( plan, stair );
}
}
}
public sealed class ArchExteriorStairEffects : IArchAffector
{
public ArchKind Kind => ArchKind.ExteriorStair;
public int Order => 60;
public int Resolve( ArchPlan plan, ArchKit kit ) => ArchExteriorStairAffector.Resolve( plan, kit );
public bool Closes( object payload ) => payload is ArchExteriorStairPart;
public void Close( ArchPlan plan, object payload )
{
if ( payload is ArchExteriorStairPart flight )
{
ArchExteriorStair.Remove( plan, flight );
}
}
}