Editor/Stair/ArchStairInteractions.cs
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;
}
ArchStairGhost.Blueprint( stair );
var shape = ArchStairShape.Resolve( stair, context.Room, context.Tool.Kit, context.Building?.Rooms, context.Building );
foreach ( var well in shape.Levels ) {
Gizmo.Draw.Color = (well.Level == shape.TopLevel ? Color.Magenta : 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 );
}
}
}