Editor/Stair/ArchStairCore.cs

Editor-side stair core and lane model types for the Arch stair system. Defines enums for walk direction, fill, step; extension methods to get headings, turns and relations; ArchStairCore that describes the shaft (origin, yaw, size, rise) and factory for straight cores; and ArchStairLane that describes an individual flight or landing with geometry helpers and copying.

File AccessNetworkingNative InteropReflection
using System.Text.Json.Serialization;

namespace Sunless.Architecture;

// Which way a flight is walked inside its core. The rectangle says WHERE the flight stands; this says how it
// is crossed, so a short wide lane can still be a flight rather than a landing.
public enum StairWalk
{
	Ahead,
	Back,
	Left,
	Right
}

// Which way a walk points in the core's own frame, and what turning it comes to. A corner is a rotation of these
// two vectors rather than sixteen cases, which is what lets one placement answer stand every step in the climb.
public static class StairWalks
{
	public static Vector2 Heading( this StairWalk walk ) => walk switch
	{
		StairWalk.Back => new Vector2( -1f, 0f ),
		StairWalk.Left => new Vector2( 0f, 1f ),
		StairWalk.Right => new Vector2( 0f, -1f ),
		_ => new Vector2( 1f, 0f )
	};

	// Width runs to the LEFT of travel, so this is the axis a step's own width is measured out on.
	public static Vector2 Leftward( this StairWalk walk )
	{
		var heading = walk.Heading();

		return new Vector2( -heading.y, heading.x );
	}

	public static StairWalk Reversed( this StairWalk walk ) => walk switch
	{
		StairWalk.Back => StairWalk.Ahead,
		StairWalk.Left => StairWalk.Right,
		StairWalk.Right => StairWalk.Left,
		_ => StairWalk.Back
	};

	public static StairWalk Turned( this StairWalk walk, StairTurn turn ) => turn switch
	{
		StairTurn.Left => walk switch
		{
			StairWalk.Ahead => StairWalk.Left,
			StairWalk.Left => StairWalk.Back,
			StairWalk.Back => StairWalk.Right,
			_ => StairWalk.Ahead
		},
		StairTurn.Right => walk switch
		{
			StairWalk.Ahead => StairWalk.Right,
			StairWalk.Right => StairWalk.Back,
			StairWalk.Back => StairWalk.Left,
			_ => StairWalk.Ahead
		},
		StairTurn.Back => walk.Reversed(),
		_ => walk
	};

	// The turn one walk makes off another, which is what a joint in the climb IS - so the drawing can show which
	// way a step already turns rather than only offering to turn it again.
	public static StairTurn Between( this StairWalk from, StairWalk to )
	{
		if ( from == to )
		{
			return StairTurn.None;
		}

		if ( from.Turned( StairTurn.Left ) == to )
		{
			return StairTurn.Left;
		}

		if ( from.Turned( StairTurn.Right ) == to )
		{
			return StairTurn.Right;
		}

		return StairTurn.Back;
	}
}

// What a flight is made of. Open is treads on stringers over an open raked soffit; Solid is a stepped mass
// standing on the core floor, which is the concrete stair and what a carved platform used to be needed for.
public enum StairFill
{
	Open,
	Solid
}

// What one step in the climb IS. A flight rakes and takes a share of the climb; a landing is level and takes
// none. Both are rectangles in the shaft's frame, which is why one ordered list is the whole stair.
public enum StairStep
{
	Flight,
	Landing
}

// The shaft. Every flight and every landing in the stair is measured in this box's frame, which is why two
// flights either abut or they do not - there is no shared square to hunt for and nothing to settle.
public sealed class ArchStairCore
{
	public Vector2 Origin { get; set; }
	public float Yaw { get; set; }
	public float Length { get; set; } = 240f;
	public float Width { get; set; } = 120f;
	// The whole climb, dragged on the box's top face or its head arrow. Pins that exceed it raise it.
	public float Rise { get; set; } = 128f;
	public StairFill Fill { get; set; }

	public ArchStairAxes Axes => new() { Origin = Origin, Yaw = Yaw };

	public Vector2 Flat( float along, float across ) => Axes.Flat( along, across );

	public List<Vector2> Outline() => Axes.Rect( 0f, Length, 0f, Width );

	public ArchStairCore Copy() => new()
	{
		Origin = Origin,
		Yaw = Yaw,
		Length = Length,
		Width = Width,
		Rise = Rise,
		Fill = Fill
	};

	// The one-lane core every code-built flight stands on - a porch step, a walkway drop, an exterior run,
	// a carved notch. They all draw one straight run, so none of them needs to know a lane from a pattern.
	public static (ArchStairCore Core, List<ArchStairLane> Lanes) Straight( Vector2 origin, Vector2 along, float length, float width, float rise )
	{
		var direction = along.IsNearZeroLength ? new Vector2( 1f, 0f ) : along.Normal;

		var core = new ArchStairCore
		{
			Origin = origin,
			Yaw = MathF.Atan2( direction.y, direction.x ).RadianToDegree(),
			Length = MathF.Max( 1f, length ),
			Width = MathF.Max( 1f, width ),
			Rise = MathF.Max( 1f, rise )
		};

		var lanes = new List<ArchStairLane>
		{
			new() { AlongFrom = 0f, AlongTo = core.Length, AcrossFrom = 0f, AcrossTo = core.Width, Walk = StairWalk.Ahead }
		};

		return (core, lanes);
	}
}

// One step of the climb - a flight or a landing - in the core's own frame. Climb order is list order, so the
// list IS the stair: flight, platform, flight, and the layer stack is a view straight onto it.
public sealed class ArchStairLane
{
	// Its own, so a railing can name the step it guards and a layer row survives being reordered.
	public int Id { get; set; }

	public StairStep Step { get; set; }

	public float AlongFrom { get; set; }
	public float AlongTo { get; set; }
	public float AcrossFrom { get; set; }
	public float AcrossTo { get; set; }
	public StairWalk Walk { get; set; }

	// How far the step is turned off the cardinal its walk names, about the corner the walk leaves from. Zero for
	// every step that ever stood in a shaft before, and what a CURVE is made of: a flight cut into segments, each
	// carrying a share of the sweep, so a swept stair is a chain of straight runs that meet at their own angles
	// rather than a second kind of geometry. The four numbers stay the step's unturned footprint - the run and the
	// width survive the turn, and the corner it turns about does not move.
	[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingDefault )]
	public float Bearing { get; set; }

	// Zero divides what the pinned flights left, the way a pillar with no height stands up to what covers it.
	// Dragging this flight's lift arrow is what pins it, and pinning one is what a mansion stair IS: three
	// risers onto a broad landing, then fourteen more. A landing takes no share unless it is pinned, which is
	// what makes it a split level rather than a turn.
	public float Rise { get; set; }

	// The railings standing on this step's own edges. Empty means the stair's Guard setting decides, which is what
	// every stair authored before railings were rows still says.
	public List<ArchStairGuardPart> Guards { get; set; } = new();

	public bool Climbs => Step == StairStep.Flight;

	public bool Guarding( StairEdge edge ) => Guards.Any( guard => guard.Edge == edge );

	// What the landing after this flight was worth before a landing was a step of its own. Read once by
	// ArchStairLanes.Settle and never written again - Normalize turns it into a real landing lane.
	[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingDefault )]
	public float LandingDepth { get; set; }

	public float AlongSpan => MathF.Max( 0f, AlongTo - AlongFrom );

	public float AcrossSpan => MathF.Max( 0f, AcrossTo - AcrossFrom );

	public bool Sideways => Walk is StairWalk.Left or StairWalk.Right;

	// The run's own length and width, which swap with the walk: a lane crossed sideways is as long as it is wide.
	public float Length => Sideways ? AcrossSpan : AlongSpan;

	public float Width => Sideways ? AlongSpan : AcrossSpan;

	// The corner the walk leaves from, in the core's own numbers. Every placement is measured out of it and a
	// bearing turns about it, so it is the one point on a step that a turn leaves where it was.
	public Vector2 Seat => Walk switch
	{
		StairWalk.Back => new Vector2( AlongTo, AcrossTo ),
		StairWalk.Left => new Vector2( AlongTo, AcrossFrom ),
		StairWalk.Right => new Vector2( AlongFrom, AcrossTo ),
		_ => new Vector2( AlongFrom, AcrossFrom )
	};

	// The corner the walk leaves from, so the run's rect covers exactly this lane travelling along its own axis
	// and across from 0 to Width - the convention every reader of ArchStairRun already keeps.
	public Vector2 Foot( ArchStairCore core ) => core.Flat( Seat.x, Seat.y );

	public float Cardinal => Walk switch
	{
		StairWalk.Back => 180f,
		StairWalk.Left => 90f,
		StairWalk.Right => -90f,
		_ => 0f
	};

	public float Yaw( ArchStairCore core ) => core.Yaw + Cardinal + Bearing;

	public ArchStairAxes Axes( ArchStairCore core ) => new() { Origin = Foot( core ), Yaw = Yaw( core ) };

	// The step read in the CORE's own frame, turned however it is turned - the one answer to where its four corners
	// actually are, so a drawing, a hit test and a shaft's bounds all read the same quad as the generator.
	public ArchStairAxes Frame => new() { Origin = Seat, Yaw = Cardinal + Bearing };

	public List<Vector2> Corners() => Frame.Rect( 0f, Length, 0f, Width );

	// Where the walk arrives, in core coordinates - the edge a landing is derived against.
	public (float AlongFrom, float AlongTo, float AcrossFrom, float AcrossTo) HeadEdge() => Walk switch
	{
		StairWalk.Back => (AlongFrom, AlongFrom, AcrossFrom, AcrossTo),
		StairWalk.Left => (AlongFrom, AlongTo, AcrossTo, AcrossTo),
		StairWalk.Right => (AlongFrom, AlongTo, AcrossFrom, AcrossFrom),
		_ => (AlongTo, AlongTo, AcrossFrom, AcrossTo)
	};

	public (float AlongFrom, float AlongTo, float AcrossFrom, float AcrossTo) FootEdge() => Walk switch
	{
		StairWalk.Back => (AlongTo, AlongTo, AcrossFrom, AcrossTo),
		StairWalk.Left => (AlongFrom, AlongTo, AcrossFrom, AcrossFrom),
		StairWalk.Right => (AlongFrom, AlongTo, AcrossTo, AcrossTo),
		_ => (AlongFrom, AlongFrom, AcrossFrom, AcrossTo)
	};

	// Which way the walk carries in core coordinates, so a derived landing knows where to grow.
	public Vector2 Heading => Frame.Along;

	// Width runs to the LEFT of travel, which is the axis the step's own width is measured out on.
	public Vector2 Leftward => Frame.Across;

	public List<Vector2> Loop( ArchStairCore core ) => Axes( core ).Rect( 0f, Length, 0f, Width );

	public StairWalk Reversed => Walk.Reversed();

	public ArchStairLane Copy() => new()
	{
		Id = Id,
		Step = Step,
		AlongFrom = AlongFrom,
		AlongTo = AlongTo,
		AcrossFrom = AcrossFrom,
		AcrossTo = AcrossTo,
		Walk = Walk,
		Bearing = Bearing,
		Rise = Rise,
		Guards = Guards.Select( guard => guard.Copy() ).ToList()
	};
}